├── .gitignore ├── README.md ├── TUTORIAL.md ├── examples ├── content_store │ ├── README.md │ └── publisher.py ├── hdmi_cec │ ├── COPYING │ ├── README.md │ ├── app │ │ ├── __init__.py │ │ ├── cec.py │ │ ├── cec_messages.proto │ │ ├── cec_messages_pb2.py │ │ ├── cec_tester.py │ │ ├── hdmi_cec_node.py │ │ ├── pir_status.py │ │ └── remote_device.py │ ├── cec_tv.py │ ├── consumer.py │ ├── pir_publisher.py │ ├── sensors │ │ ├── __init__.py │ │ ├── fake_pir.py │ │ ├── led.py │ │ └── pir.py │ ├── small.mp4 │ └── util │ │ ├── __init__.py │ │ └── common.py └── led_control │ ├── README.md │ ├── led_multi_node.py │ ├── led_node.py │ └── led_user.py ├── iot_controller.sample ├── ndn-iot-console ├── ndn-iot-controller ├── ndn-iot-start ├── ndn-wifi-passwd └── ndn_pi ├── __init__.py ├── base_node.py ├── commands ├── __init__.py ├── cert-request.proto ├── cert_request_pb2.py ├── configure-device.proto ├── configure_device_pb2.py ├── send-pairing-info.proto ├── send_pairing_info_pb2.py ├── update-capabilities.proto └── update_capabilities_pb2.py ├── dialog.py ├── iot_console.py ├── iot_controller.py ├── iot_node.py └── security ├── .default.conf ├── __init__.py ├── hmac_helper.py ├── iot_identity_manager.py ├── iot_identity_storage.py ├── iot_policy_manager.py ├── iot_private_key_storage.py └── sha256_hmac_signature.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Named Data Network Internet of Things Toolkit (NDN-IoTT) 2 | ========================== 3 | 4 | *** 5 | Update: Due to a lot of recent interest in this toolkit, there is now [a form here](https://goo.gl/forms/WDTG0Xtx2OT4KL0h1) for anyone who is running into difficulties installing or using ndn-pi. Based on the responses, we may update the project. 6 | 7 | While you wait for an update, some users have reported success using current versions of PyNDN2 and copying parts of the [updated ndn-pi framework](https://github.com/remap/ndn-flow/tree/master/framework/ndn_pi) used in the Flow application. 8 | 9 | *** 10 | 11 | Getting Started 12 | --------------------------------- 13 | 14 | The major components of this kit are: 15 | - PyNDN: a Python implementation of NDN 16 | - nfd: the NDN Forwarding Daemon, which manages connections (faces) 17 | - nrd: the NDN Routing Daemon, which routes interests and data 18 | 19 | There are other libraries included for further exploration of NDN: 20 | - repo-ng: a data repository server 21 | - ndn-cpp: C++ implementation of NDN 22 | - ndn-cxx: C++ implementation of NDN with eXperimental eXtensions 23 | 24 | ### Network Connectivity 25 | 26 | In order to communicate using NDN, all devices, Raspberry Pi or otherwise, must be 27 | connected to the same LAN. By default, Raspberry Pis are configured to create or join 28 | a WiFi network named 'Raspi\_NDN' if a wireless interface is available. 29 | 30 | The default password for 'Raspi\_NDN' is 'defaultpasswd'. It can be changed with the ndn-wifi-passwd tool, 31 | or by modifying /etc/hostapd/hostapd.conf and /etc/wpa_supplicant/wpa_supplicant.conf. 32 | 33 | Alternatively, you may connect your Raspberry Pis by Ethernet. 34 | 35 | ### Network Configuration 36 | 37 | If you are using multiple Raspberry Pis, they must all be connected to the same network, whether by WiFi 38 | or Ethernet. This allows interests and data to be multicast to the other nodes over UDP. To set up multicast, 39 | you must register your network an NDN multicast face. 40 | 41 | There is an installed script, ndn-iot-start, that will start the NDN forwarder and router if they are not 42 | already running, and automatically route traffic from your nodes to the multicast face. It assumes that your 43 | Pis are connected to a WiFi network, using the 'wlan0' interface. If you are using a different interface, e.g. 44 | 'eth0' for ethernet, you may run 45 | 46 | ndn-iot-start -i eth0 47 | 48 | replacing 'eth0' with the desired interface name. For a list of network interfaces on your Pi, run 'ifconfig'. 49 | 50 | If you wish to configure routing yourself **(not recommended)**, see [below](#manually-configuring-routing). 51 | 52 | 53 | ### Running Your Iot Nodes 54 | 55 | The basic unit of the IoT toolkit network is a node. Nodes are virtual, in that one 56 | machine may host multiple simple nodes instead of one multi-purpose node. Although the functions of a node are 57 | completely up to the user, we recommend using each node to group related commands. For example, a Raspberry Pi 58 | with both LEDs and infrared sensors may run one node that responds to LED control commands, and another that 59 | reports proximity readings from the IR sensors. 60 | 61 | There is one special node type, the controller. Each network must have a controller. Its primary responsibilities 62 | are creating network certificates for all other nodes in the network, and maintaining a list of available 63 | services. 64 | 65 | The configuration for the controller consists of just the network name (default is '/home') and the controller name 66 | (default is 'controller'). The default configuration file can be in /home/pi/.ndn/controller.conf. To change controller settings, you 67 | may edit this file, or run the included ndn-iot-controller script: 68 | 69 | ndn-iot-controller 70 | 71 | When nodes other than the controller join the network, they must be added by the user, by providing a serial number and 72 | PIN. This prevents unknown machines from gaining access to protected network commands. Use the menu provided by the controller to pair the new 73 | node by entering 'P'. You will be prompted for the serial, PIN and a new name for your node. After a few seconds, the node will 74 | finish its setup handshake with the controller and be ready to interact with the other nodes. You can use 'D' for 'directory' to 75 | see the commands available on the new node. 76 | 77 | **Note:** Although multiple nodes may run on a single Raspberry Pi, the traffic from three or more nodes slow nfd down 78 | considerably, depending on the model of the Pi. 79 | 80 | **Note:** The directory may not correctly reflect the presence of multiple nodes with the same name. This limitation should be fixed in later 81 | versions. 82 | 83 | ### Examples 84 | 85 | This toolkit contains three examples that demonstrate common node and network setups. 86 | - led\_control: Control LEDs connected to the general purpose input/output (GPIO) pins over the network 87 | - hdmi\_cec: Turn a CEC-enabled device on or off depending on room occupancy 88 | - content\_store: Save device statistics in a MemoryContentCache object for later analysis or logging 89 | 90 | Try running these examples and going through the tutorial [TUTORIAL.md](TUTORIAL.md) to learn how nodes work together. 91 | 92 | **Note: In order to access the GPIO pins, an IotNode must be run as root.** 93 | 94 | ### Manually configuring routing 95 | 96 | It is recommended that you use the included script, ndn-iot-start, but you can manually set up routing on your nodes with the following 97 | steps. 98 | 99 | 1. Ensure that the Raspberry Pi is connected to the network (wired or wireless) that will host your IoT network. 100 | 101 | 2. Start the NDN forwarder and router by running 102 | 103 | nfd-start 104 | 105 | 3. Tell the forwarder to route network traffic to the multicast face. 106 | If you are using WiFi only with your Raspberry Pi, the multicast face will typically have faceid 2. Otherwise, 107 | you will need to use the 'nfd-status' command to determine the correct face to register. Run 108 | 109 | nfd-status -f 110 | 111 | and look for lines containing `remote=udp4://224.0.23.170:56363`. Find the faceid 112 | that contains an IP address on the IoT network. For example, if your nodes are all on a WiFi network, and your 113 | WiFi IP address is 192.168.16.7, you may find a line that reads 114 | 115 | faceid=3 remote=udp4://224.0.23.170:56363 local=udp4://192.168.16.7:56356 ... 116 | 117 | 4. Tell the forwarder to route traffic to the face you discovered in *3*. 118 | 119 | nfdc-register / 120 | 121 | 122 | Writing IoT Nodes 123 | ---------------- 124 | 125 | 126 | ### Provided Classes 127 | There are several classes provided as part of the Internet of Things toolkit for NDN. 128 | #### IotNode 129 | 130 | Nodes in your network will generally be subclasses of IotNode. 131 | 132 | The most important method for customizing nodes is `addCommand`: 133 | 134 | ```python 135 | def addCommand(suffix, func, keywords, isSigned) 136 | ``` 137 | This is used to register your custom interest handling methods. The parameters are: 138 | - `suffix`: An NDN name that will be added to the node prefix to form the full command name. 139 | - `func`: A function that is called whenever the node receives an interest matching the suffix. The 140 | function must take an Interest object and return a Data object: 141 | 142 | ```python 143 | # returns pyndn.Data or None 144 | def handlerFunction(interest): 145 | dataName = Name(interest.getName()) 146 | # ... do some processing based on the interest 147 | # return a Data object or the interest will time out 148 | response = Data(dataName) 149 | response.setContent('Done') 150 | return response 151 | ``` 152 | Note that the sender of the interest will not receive your reply if the name of the data object does not match 153 | the interest name. That is, you may append components to `dataName`, but not remove them. You may also return `None`, 154 | which will cause the interest to time out. 155 | 156 | - `keywords`: A list of strings. The controller groups together all commands that share a keyword, so that other nodes 157 | can search for a particular capability, service, sensor type, etc. You are free to define as many keywords as you like, 158 | and their meaning is mainly application-dependent. 159 | - `isSigned`: By default, this is set to `False`. Setting this to `True` will allow only devices who are part of your network to 160 | invoke the command, by signing their command interests. 161 | 162 | Besides adding methods for interest handling with `addCommand`, nodes can be further customized by overriding the 163 | following methods: 164 | 165 | * setupComplete 166 | ```python 167 | def setupComplete(self) 168 | ``` 169 | This method does nothing by default. It is called once the node has received its network 170 | certificate from the controller and sent its capabilities list. This is the recommended 171 | place for customized node behavior, e.g. searching for other nodes, scheduling tasks, 172 | setting up custom callbacks. 173 | 174 | * unknownCommandResponse 175 | ```python 176 | #returns pyndn.Data or None 177 | def unknownCommandResponse(self, interest) 178 | ``` 179 | By default, this method composes an error message and adds 'unknown' to the end of the 180 | interest name. You may return `None` to silently ignore the unknown interest, or perform 181 | your own specialized handling of the interest and return a Data packet. 182 | 183 | * verificationFailed 184 | ```python 185 | def verificationFailed(self, dataOrInterest) 186 | ``` 187 | Called when a command interest fails verification. The most common reasons for failing verification are invalid signatures, 188 | and unsigned interests being sent when signed interests are expected. The default implementation logs the failure. 189 | 190 | * getSerial 191 | ```python 192 | def getSerial(self) 193 | ``` 194 | Reads the Raspberry Pi serial number from /proc/cpuinfo. You may override this to provide some other unique id for your 195 | Raspberry Pis or even individual IotNodes. 196 | 197 | ------ 198 | 199 | The remaining classes do not need to be subclassed, and it is not recommended that you modify them 200 | before you are comfortable with the toolkit and with NDN security management. For more information, 201 | see [NDN Resources](#ndn-resources). 202 | 203 | #### IoT Network Classes 204 | 205 | * BaseNode 206 | * IotController 207 | * IotConsole 208 | 209 | #### Security Classes 210 | * HmacHelper 211 | * IotPolicyManager 212 | * IotIdentityManager 213 | * IotIdentityStorage 214 | * IotPrivateKeyStorage 215 | 216 | NDN Resources 217 | ----------------- 218 | 219 | * [NDN Common Client Libraries](http://named-data.net/doc/ndn-ccl-api/) for documentation of the classes available in PyNDN 220 | * [ndn-cxx wiki](http://redmine.named-data.net/projects/ndn-cxx/wiki) for security information 221 | * [NFD wiki](http://redmine.named-data.net/projects/nfd/wiki) for more on the internals of NDN packets and forwarding 222 | 223 | -------------------------------------------------------------------------------- /TUTORIAL.md: -------------------------------------------------------------------------------- 1 | 2 | Named Data Network Internet of Things Toolkit (NDN-IOTT) 3 | ======= 4 | Tutorial 5 | ========= 6 | 7 | This tutorial will walk you through configuring and running your own IoT network over NDN. 8 | Before following this tutorial, try running the 'led\_control' example in the examples folder. 9 | 10 | Copy the 'led\_control' folder to a new location so you can make changes. 11 | 12 | Extending the LED control example 13 | ------------------------------------- 14 | 15 | ### Adding LEDs 16 | The easiest way to extend the LED control network is to add another node that provides the same type of service, controlling an LED. 17 | You may attach an LED to a different pin (e.g. pin 17), or run another node controlling the same pin. You may add this new node to 18 | any Raspberry Pi in your network (including one already running an LED node). 19 | 20 | To start the new node, run 21 | 22 | sudo ./led_node.py [pin] 23 | 24 | with an optional pin number. If no pin number is given, 24 is assumed. After pairing the new node with the controller, enter 'D' 25 | in the controller menu. You will see new entries for the new node you have registered. Try sending interests 26 | to turn the LED on and off. 27 | 28 | Another way to extend the network is to add more LEDs to the multi-LED node. 29 | Open 'led_multi_node.py' and add another pin to the pinList. Save and run the 30 | new node with 31 | 32 | sudo ./led_multi_node.py 33 | 34 | Now the controller directory should show three different pins available for control on the multi-LED node. 35 | 36 | ### Adding commands to existing nodes 37 | 38 | You can also add commands to the nodes to increase their capabilities. Open up 'led\_node.py'. You will see there is an 'onBlinkCommand' 39 | method that is not currently used. This method toggles blinking on the controlled LED. To install this command, 40 | add the following to the end of the __init__ method: 41 | 42 | ```python 43 | blinkCommand = Name('toggleBlink') 44 | self.addCommand(blinkCommand, self.onBlinkCommand, ['led', 'light'], False) 45 | ``` 46 | 47 | Now run the node and pair it with the controller as usual. You will now see the blink command 48 | in the directory. Try sending interests to turn blinking on and off. 49 | 50 | ### Requiring signed commands 51 | 52 | The final change you can make to a node is to require signing for its commands. Signed commands 53 | contain keys that can be used to determine the identity of the sender. To require signing, simply change the `False` at the end of the `addCommand` lines to `True`. 54 | 55 | Now, when you run the node and pair with the controller, your commands will only be 56 | successful if you choose to sign them. To sign commands sent in source code, you must call 57 | 58 | self.face.makeCommandInterest(interest) 59 | 60 | just before expressing the interest. 61 | -------------------------------------------------------------------------------- /examples/content_store/README.md: -------------------------------------------------------------------------------- 1 | 2 | Content Publisher with Cache 3 | ============================ 4 | 5 | 6 | This example collects system information such as memory and CPU usage and stores them 7 | into a content cache for later retrieval. 8 | 9 | Node Types 10 | ---------- 11 | 12 | ###Controller 13 | 14 | The controller in this network has no special function: it listens for certificate requests and device listing requests. 15 | 16 | ### Publisher Node 17 | 18 | This node directly controls an LED attached to a GPIO pin (by default, 24). 19 | 20 | 21 | Setup 22 | ------- 23 | 24 | No special hardware setup is needed for this example. 25 | 26 | ### Network Setup 27 | See the README.md in (ndn-pi path?) for NDN setup steps. 28 | 29 | Running the Example 30 | ------------------- 31 | 32 | The controller node should be started first, using: 33 | 34 | python -m ndn_pi.iot_controller & 35 | 36 | Then the publisher can be started using 37 | 38 | ./publisher.py & 39 | 40 | Finally, we run a console node using: 41 | 42 | python -m ndn_pi.iot_console & 43 | 44 | 45 | The console node periodically polls the controller for available commands and presents them to the user. If 46 | the publisher was started successfully, you should see an entry like: 47 | ``` 48 | repo: 49 | /home/repoman/listDataPrefixes 50 | ``` 51 | 52 | If you enter this as written (don't add any spaces!), you should receive a JSON list containing all 53 | data prefixes that the repo is listening on. Right now, there is only one, `/home/repoman/data`. 54 | 55 | If you now enter `/home/rempoman/data` into the console, you will receive the latest data from the content cache. 56 | You will notice that the name of the data (after 'Received: ') is the interest name you provided with an extra 57 | component at the end. If you wish to retreive the same data object in the future, you can send an interest with its 58 | full name. Otherwise, the content cache will continue to return new data as it is created. 59 | -------------------------------------------------------------------------------- /examples/content_store/publisher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # 4 | # This is the data publisher using MemoryContentCache 5 | # 6 | 7 | from pyndn import Name, Face, Interest, Data 8 | from pyndn.util.memory_content_cache import MemoryContentCache 9 | 10 | import time 11 | import sys 12 | import psutil as ps 13 | import json 14 | from ndn_pi.iot_node import IotNode 15 | 16 | import logging 17 | class CachedContentPublisher(IotNode): 18 | def __init__(self): 19 | super(CachedContentPublisher, self).__init__() 20 | self._missedRequests = 0 21 | self._dataPrefix = None 22 | self.addCommand(Name('listPrefixes'), self.listDataPrefixes, ['repo'], 23 | False) 24 | 25 | def setupComplete(self): 26 | # The cache will clear old values every 100s 27 | self._dataCache = MemoryContentCache(self.face, 100000) 28 | self._dataPrefix = Name(self.prefix).append('data') 29 | self.registerCachePrefix() 30 | print "Serving data at {}".format(self._dataPrefix.toUri()) 31 | self.loop.call_soon(self.publishData) 32 | 33 | def listDataPrefixes(self, interest): 34 | d = Data(interest.getName()) 35 | if self._dataPrefix is not None: 36 | d.setContent(json.dumps([self._dataPrefix.toUri()])) 37 | d.getMetaInfo().setFreshnessPeriod(10000) 38 | return d 39 | 40 | def registerCachePrefix(self): 41 | self._dataCache.registerPrefix(self._dataPrefix, self.cacheRegisterFail , self.onDataMissing) 42 | 43 | def unknownCommandResponse(self, interest): 44 | # we override this so the MemoryContentCache can handle data requests 45 | afterPrefix = interest.getName().get(self.prefix.size()).toEscapedString() 46 | if afterPrefix == 'data': 47 | return None 48 | else: 49 | return super(CachedContentPublisher, self).unknownCommandResponse(interest) 50 | 51 | def cacheRegisterFail(self, interest): 52 | # just try again 53 | self.log.warn('Could not register data cache') 54 | self.registerCachePrefix() 55 | 56 | def onDataMissing(self, prefix, interest, transport, prefixId): 57 | self._missedRequests += 1 58 | # let it timeout 59 | 60 | def publishData(self): 61 | timestamp = time.time() 62 | cpu_use = ps.cpu_percent() 63 | users = [u.name for u in ps.users()] 64 | nProcesses = len(ps.pids()) 65 | memUse = ps.virtual_memory().percent 66 | swapUse = ps.swap_memory().percent 67 | 68 | info = {'cpu_usage':cpu_use, 'users':users, 'processes':nProcesses, 69 | 'memory_usage':memUse, 'swap_usage':swapUse} 70 | 71 | dataOut = Data(Name(self._dataPrefix).appendVersion(int(timestamp))) 72 | dataOut.setContent(json.dumps(info)) 73 | dataOut.getMetaInfo().setFreshnessPeriod(10000) 74 | self.signData(dataOut) 75 | 76 | self._dataCache.add(dataOut) 77 | 78 | # repeat every 5 seconds 79 | self.loop.call_later(5, self.publishData) 80 | 81 | if __name__ == '__main__': 82 | n = CachedContentPublisher() 83 | n.start() 84 | -------------------------------------------------------------------------------- /examples/hdmi_cec/README.md: -------------------------------------------------------------------------------- 1 | HDMI-CEC TV controller 2 | ====================== 3 | 4 | 5 | This network reads any available passive infrared sensors (PIRs) to determine room occupancy. 6 | It turns connected TVs off if all sensors report no occupancy, and turns TVs on if more than 2 7 | nodes report occupancy. 8 | 9 | Node Types 10 | ---------- 11 | 12 | ###Controller 13 | 14 | The controller in this network has no special function: it listens for certificate requests and device listing requests. 15 | 16 | ###PIR Publisher Node 17 | 18 | This node handles requests for PIR status by reading PIRs attached to the GPIO pins (by default 25 and 18). 19 | 20 | ###CEC TV Node 21 | 22 | This node sends commands to a CEC-enabled device attached to the HDMI output of the Raspberry Pi. 23 | 24 | ### Consumer Node 25 | This node searches for nodes with 'pir' and 'cec' capabilities. It periodically reads the status of all PIR sensors, 26 | and issues commands to the TV. 27 | 28 | Setup 29 | ------- 30 | 31 | ### PIR Publisher 32 | This node expects two passive infrared sensors connected to pins 25 and 18. The number and placement of PIR sensors 33 | can be changed by editing 'pir.conf' with `ndn-config`. 34 | 35 | 36 | 37 | ### CEC TV Node 38 | This node expects to be connected to an HDMI-CEC enabled television/monitor. 39 | 40 | ### Network Setup 41 | See the README.md in (ndn-pi path?) for NDN setup steps. 42 | 43 | Running the Example 44 | ------------------- 45 | 46 | The controller node should be started first, using: 47 | 48 | python -m ndn_pi.iot_controller & 49 | 50 | The CEC controller and PIR publisher nodes require root access: 51 | 52 | sudo -E ./cec_tv.py cec_tv.conf & 53 | 54 | and 55 | 56 | sudo -E ./pir_publisher.py pir.conf & 57 | 58 | 59 | The consumer can be run using 60 | 61 | ./consumer.py consumer.conf& 62 | 63 | -------------------------------------------------------------------------------- /examples/hdmi_cec/app/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Regents of the University of California. 2 | # Author: Spencer Sutterlin 3 | # 4 | # This file is part of ndn-pi (Named Data Networking - Pi). 5 | # 6 | # ndn-pi is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # A copy of the GNU General Public License is in the file COPYING. 19 | 20 | -------------------------------------------------------------------------------- /examples/hdmi_cec/app/cec.py: -------------------------------------------------------------------------------- 1 | 2 | # -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */ 3 | # 4 | # Copyright (C) 2014 Regents of the University of California. 5 | # Author: Adeola Bannis 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # A copy of the GNU General Public License is in the file COPYING. 20 | 21 | class CecCommand(object): 22 | STANDBY = "36" 23 | ON = "04" 24 | PLAY = "41:24" 25 | PAUSE = "41:25" 26 | FF = "41:05" 27 | RW = "41:09" 28 | SEL = "44:00" 29 | UP = "44:01" 30 | DOWN = "44:02" 31 | LEFT = "44:03" 32 | RIGHT = "44:04" 33 | TVMENU = "44:09" 34 | DVDMENU = "44:05" 35 | 36 | # TODO: volume controls not working 37 | 38 | class CecDevice(object): 39 | TV = 0 40 | RECORDING_1 = 1 41 | RECORDING_2 = 2 42 | TUNER_1 = 3 43 | PLAYBACK_1 = 4 44 | AUDIO_SYSTEM = 5 45 | TUNER_2 = 6 46 | TUNER_3 = 7 47 | PLAYBACK_2 = 8 48 | PLAYBACK_3 = 9 49 | TUNER_4 = 10 50 | PLAYBACK_3 = 11 51 | RESERVED_C = 12 52 | RESERVED_D = 13 53 | RESERVED_E = 14 54 | BROADCAST = 15 55 | 56 | class CecStatus(object): 57 | def __init__(self): 58 | self._power = False 59 | 60 | def __repr__(self): 61 | return "CecStatus(power:{0})".format(self._power) 62 | -------------------------------------------------------------------------------- /examples/hdmi_cec/app/cec_messages.proto: -------------------------------------------------------------------------------- 1 | // Compile this file using: 2 | // protoc --python_out=. cec_messages.proto 3 | 4 | package ndn_pi_message; 5 | 6 | enum Device { 7 | TV = 0; 8 | RECORDING_1 = 1; 9 | PLAYBACK_1 = 4; 10 | RESERVED_E = 14; 11 | BROADCAST = 15; 12 | } 13 | 14 | enum Command { 15 | STANDBY = 0; 16 | ON = 1; 17 | PLAY = 2; 18 | PAUSE = 3; 19 | FF = 4; 20 | RW = 5; 21 | SEL = 6; 22 | UP = 7; 23 | DOWN = 8; 24 | LEFT = 9; 25 | RIGHT = 10; 26 | AS = 11; 27 | SLEEP = 12; 28 | TVMENU = 13; 29 | DVDMENU = 14; 30 | } 31 | 32 | message DevicesCapabilitiesMessage { 33 | message DeviceCapabilities { 34 | required Device device = 1; 35 | repeated Command capabilities = 2; 36 | } 37 | 38 | repeated DeviceCapabilities deviceCapabilities = 1; 39 | } 40 | 41 | message CommandMessage { 42 | required Device destination = 1; 43 | repeated Command commands = 2; 44 | } 45 | -------------------------------------------------------------------------------- /examples/hdmi_cec/app/cec_messages_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | 3 | from google.protobuf import descriptor 4 | from google.protobuf import message 5 | from google.protobuf import reflection 6 | from google.protobuf import descriptor_pb2 7 | # @@protoc_insertion_point(imports) 8 | 9 | 10 | 11 | DESCRIPTOR = descriptor.FileDescriptor( 12 | name='cec_messages.proto', 13 | package='ndn_pi_message', 14 | serialized_pb='\n\x12\x63\x65\x63_messages.proto\x12\x0endn_pi_message\"\xe4\x01\n\x1a\x44\x65vicesCapabilitiesMessage\x12Y\n\x12\x64\x65viceCapabilities\x18\x01 \x03(\x0b\x32=.ndn_pi_message.DevicesCapabilitiesMessage.DeviceCapabilities\x1ak\n\x12\x44\x65viceCapabilities\x12&\n\x06\x64\x65vice\x18\x01 \x02(\x0e\x32\x16.ndn_pi_message.Device\x12-\n\x0c\x63\x61pabilities\x18\x02 \x03(\x0e\x32\x17.ndn_pi_message.Command\"h\n\x0e\x43ommandMessage\x12+\n\x0b\x64\x65stination\x18\x01 \x02(\x0e\x32\x16.ndn_pi_message.Device\x12)\n\x08\x63ommands\x18\x02 \x03(\x0e\x32\x17.ndn_pi_message.Command*P\n\x06\x44\x65vice\x12\x06\n\x02TV\x10\x00\x12\x0f\n\x0bRECORDING_1\x10\x01\x12\x0e\n\nPLAYBACK_1\x10\x04\x12\x0e\n\nRESERVED_E\x10\x0e\x12\r\n\tBROADCAST\x10\x0f*\x9f\x01\n\x07\x43ommand\x12\x0b\n\x07STANDBY\x10\x00\x12\x06\n\x02ON\x10\x01\x12\x08\n\x04PLAY\x10\x02\x12\t\n\x05PAUSE\x10\x03\x12\x06\n\x02\x46\x46\x10\x04\x12\x06\n\x02RW\x10\x05\x12\x07\n\x03SEL\x10\x06\x12\x06\n\x02UP\x10\x07\x12\x08\n\x04\x44OWN\x10\x08\x12\x08\n\x04LEFT\x10\t\x12\t\n\x05RIGHT\x10\n\x12\x06\n\x02\x41S\x10\x0b\x12\t\n\x05SLEEP\x10\x0c\x12\n\n\x06TVMENU\x10\r\x12\x0b\n\x07\x44VDMENU\x10\x0e') 15 | 16 | _DEVICE = descriptor.EnumDescriptor( 17 | name='Device', 18 | full_name='ndn_pi_message.Device', 19 | filename=None, 20 | file=DESCRIPTOR, 21 | values=[ 22 | descriptor.EnumValueDescriptor( 23 | name='TV', index=0, number=0, 24 | options=None, 25 | type=None), 26 | descriptor.EnumValueDescriptor( 27 | name='RECORDING_1', index=1, number=1, 28 | options=None, 29 | type=None), 30 | descriptor.EnumValueDescriptor( 31 | name='PLAYBACK_1', index=2, number=4, 32 | options=None, 33 | type=None), 34 | descriptor.EnumValueDescriptor( 35 | name='RESERVED_E', index=3, number=14, 36 | options=None, 37 | type=None), 38 | descriptor.EnumValueDescriptor( 39 | name='BROADCAST', index=4, number=15, 40 | options=None, 41 | type=None), 42 | ], 43 | containing_type=None, 44 | options=None, 45 | serialized_start=375, 46 | serialized_end=455, 47 | ) 48 | 49 | 50 | _COMMAND = descriptor.EnumDescriptor( 51 | name='Command', 52 | full_name='ndn_pi_message.Command', 53 | filename=None, 54 | file=DESCRIPTOR, 55 | values=[ 56 | descriptor.EnumValueDescriptor( 57 | name='STANDBY', index=0, number=0, 58 | options=None, 59 | type=None), 60 | descriptor.EnumValueDescriptor( 61 | name='ON', index=1, number=1, 62 | options=None, 63 | type=None), 64 | descriptor.EnumValueDescriptor( 65 | name='PLAY', index=2, number=2, 66 | options=None, 67 | type=None), 68 | descriptor.EnumValueDescriptor( 69 | name='PAUSE', index=3, number=3, 70 | options=None, 71 | type=None), 72 | descriptor.EnumValueDescriptor( 73 | name='FF', index=4, number=4, 74 | options=None, 75 | type=None), 76 | descriptor.EnumValueDescriptor( 77 | name='RW', index=5, number=5, 78 | options=None, 79 | type=None), 80 | descriptor.EnumValueDescriptor( 81 | name='SEL', index=6, number=6, 82 | options=None, 83 | type=None), 84 | descriptor.EnumValueDescriptor( 85 | name='UP', index=7, number=7, 86 | options=None, 87 | type=None), 88 | descriptor.EnumValueDescriptor( 89 | name='DOWN', index=8, number=8, 90 | options=None, 91 | type=None), 92 | descriptor.EnumValueDescriptor( 93 | name='LEFT', index=9, number=9, 94 | options=None, 95 | type=None), 96 | descriptor.EnumValueDescriptor( 97 | name='RIGHT', index=10, number=10, 98 | options=None, 99 | type=None), 100 | descriptor.EnumValueDescriptor( 101 | name='AS', index=11, number=11, 102 | options=None, 103 | type=None), 104 | descriptor.EnumValueDescriptor( 105 | name='SLEEP', index=12, number=12, 106 | options=None, 107 | type=None), 108 | descriptor.EnumValueDescriptor( 109 | name='TVMENU', index=13, number=13, 110 | options=None, 111 | type=None), 112 | descriptor.EnumValueDescriptor( 113 | name='DVDMENU', index=14, number=14, 114 | options=None, 115 | type=None), 116 | ], 117 | containing_type=None, 118 | options=None, 119 | serialized_start=458, 120 | serialized_end=617, 121 | ) 122 | 123 | 124 | TV = 0 125 | RECORDING_1 = 1 126 | PLAYBACK_1 = 4 127 | RESERVED_E = 14 128 | BROADCAST = 15 129 | STANDBY = 0 130 | ON = 1 131 | PLAY = 2 132 | PAUSE = 3 133 | FF = 4 134 | RW = 5 135 | SEL = 6 136 | UP = 7 137 | DOWN = 8 138 | LEFT = 9 139 | RIGHT = 10 140 | AS = 11 141 | SLEEP = 12 142 | TVMENU = 13 143 | DVDMENU = 14 144 | 145 | 146 | 147 | _DEVICESCAPABILITIESMESSAGE_DEVICECAPABILITIES = descriptor.Descriptor( 148 | name='DeviceCapabilities', 149 | full_name='ndn_pi_message.DevicesCapabilitiesMessage.DeviceCapabilities', 150 | filename=None, 151 | file=DESCRIPTOR, 152 | containing_type=None, 153 | fields=[ 154 | descriptor.FieldDescriptor( 155 | name='device', full_name='ndn_pi_message.DevicesCapabilitiesMessage.DeviceCapabilities.device', index=0, 156 | number=1, type=14, cpp_type=8, label=2, 157 | has_default_value=False, default_value=0, 158 | message_type=None, enum_type=None, containing_type=None, 159 | is_extension=False, extension_scope=None, 160 | options=None), 161 | descriptor.FieldDescriptor( 162 | name='capabilities', full_name='ndn_pi_message.DevicesCapabilitiesMessage.DeviceCapabilities.capabilities', index=1, 163 | number=2, type=14, cpp_type=8, label=3, 164 | has_default_value=False, default_value=[], 165 | message_type=None, enum_type=None, containing_type=None, 166 | is_extension=False, extension_scope=None, 167 | options=None), 168 | ], 169 | extensions=[ 170 | ], 171 | nested_types=[], 172 | enum_types=[ 173 | ], 174 | options=None, 175 | is_extendable=False, 176 | extension_ranges=[], 177 | serialized_start=160, 178 | serialized_end=267, 179 | ) 180 | 181 | _DEVICESCAPABILITIESMESSAGE = descriptor.Descriptor( 182 | name='DevicesCapabilitiesMessage', 183 | full_name='ndn_pi_message.DevicesCapabilitiesMessage', 184 | filename=None, 185 | file=DESCRIPTOR, 186 | containing_type=None, 187 | fields=[ 188 | descriptor.FieldDescriptor( 189 | name='deviceCapabilities', full_name='ndn_pi_message.DevicesCapabilitiesMessage.deviceCapabilities', index=0, 190 | number=1, type=11, cpp_type=10, label=3, 191 | has_default_value=False, default_value=[], 192 | message_type=None, enum_type=None, containing_type=None, 193 | is_extension=False, extension_scope=None, 194 | options=None), 195 | ], 196 | extensions=[ 197 | ], 198 | nested_types=[_DEVICESCAPABILITIESMESSAGE_DEVICECAPABILITIES, ], 199 | enum_types=[ 200 | ], 201 | options=None, 202 | is_extendable=False, 203 | extension_ranges=[], 204 | serialized_start=39, 205 | serialized_end=267, 206 | ) 207 | 208 | 209 | _COMMANDMESSAGE = descriptor.Descriptor( 210 | name='CommandMessage', 211 | full_name='ndn_pi_message.CommandMessage', 212 | filename=None, 213 | file=DESCRIPTOR, 214 | containing_type=None, 215 | fields=[ 216 | descriptor.FieldDescriptor( 217 | name='destination', full_name='ndn_pi_message.CommandMessage.destination', index=0, 218 | number=1, type=14, cpp_type=8, label=2, 219 | has_default_value=False, default_value=0, 220 | message_type=None, enum_type=None, containing_type=None, 221 | is_extension=False, extension_scope=None, 222 | options=None), 223 | descriptor.FieldDescriptor( 224 | name='commands', full_name='ndn_pi_message.CommandMessage.commands', index=1, 225 | number=2, type=14, cpp_type=8, label=3, 226 | has_default_value=False, default_value=[], 227 | message_type=None, enum_type=None, containing_type=None, 228 | is_extension=False, extension_scope=None, 229 | options=None), 230 | ], 231 | extensions=[ 232 | ], 233 | nested_types=[], 234 | enum_types=[ 235 | ], 236 | options=None, 237 | is_extendable=False, 238 | extension_ranges=[], 239 | serialized_start=269, 240 | serialized_end=373, 241 | ) 242 | 243 | _DEVICESCAPABILITIESMESSAGE_DEVICECAPABILITIES.fields_by_name['device'].enum_type = _DEVICE 244 | _DEVICESCAPABILITIESMESSAGE_DEVICECAPABILITIES.fields_by_name['capabilities'].enum_type = _COMMAND 245 | _DEVICESCAPABILITIESMESSAGE_DEVICECAPABILITIES.containing_type = _DEVICESCAPABILITIESMESSAGE; 246 | _DEVICESCAPABILITIESMESSAGE.fields_by_name['deviceCapabilities'].message_type = _DEVICESCAPABILITIESMESSAGE_DEVICECAPABILITIES 247 | _COMMANDMESSAGE.fields_by_name['destination'].enum_type = _DEVICE 248 | _COMMANDMESSAGE.fields_by_name['commands'].enum_type = _COMMAND 249 | DESCRIPTOR.message_types_by_name['DevicesCapabilitiesMessage'] = _DEVICESCAPABILITIESMESSAGE 250 | DESCRIPTOR.message_types_by_name['CommandMessage'] = _COMMANDMESSAGE 251 | 252 | class DevicesCapabilitiesMessage(message.Message): 253 | __metaclass__ = reflection.GeneratedProtocolMessageType 254 | 255 | class DeviceCapabilities(message.Message): 256 | __metaclass__ = reflection.GeneratedProtocolMessageType 257 | DESCRIPTOR = _DEVICESCAPABILITIESMESSAGE_DEVICECAPABILITIES 258 | 259 | # @@protoc_insertion_point(class_scope:ndn_pi_message.DevicesCapabilitiesMessage.DeviceCapabilities) 260 | DESCRIPTOR = _DEVICESCAPABILITIESMESSAGE 261 | 262 | # @@protoc_insertion_point(class_scope:ndn_pi_message.DevicesCapabilitiesMessage) 263 | 264 | class CommandMessage(message.Message): 265 | __metaclass__ = reflection.GeneratedProtocolMessageType 266 | DESCRIPTOR = _COMMANDMESSAGE 267 | 268 | # @@protoc_insertion_point(class_scope:ndn_pi_message.CommandMessage) 269 | 270 | # @@protoc_insertion_point(module_scope) 271 | -------------------------------------------------------------------------------- /examples/hdmi_cec/app/cec_tester.py: -------------------------------------------------------------------------------- 1 | import time 2 | import subprocess 3 | import sys 4 | 5 | from app.cec import CecDevice, CecCommand 6 | 7 | PI = CecDevice.RECORDING_1 8 | 9 | def sendCommand(command): 10 | print command 11 | proc = subprocess.Popen(["cec-client", "-s", "-d", "1"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) 12 | (out, err) = proc.communicate(input=command) 13 | print "OUT:", out 14 | print "ERR:", err 15 | 16 | if __name__ == "__main__": 17 | if len(sys.argv) > 1: 18 | sendCommand(sys.argv[1:]) 19 | else: 20 | command = "tx " + format(PI, '01x') + format(CecDevice.TV, '01x') + ":" + CecCommand.ON 21 | sendCommand(command) 22 | -------------------------------------------------------------------------------- /examples/hdmi_cec/app/hdmi_cec_node.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Regents of the University of California. 2 | # Author: Spencer Sutterlin 3 | # 4 | # This file is part of ndn-pi (Named Data Networking - Pi). 5 | # 6 | # ndn-pi is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # A copy of the GNU General Public License is in the file COPYING. 19 | 20 | from pyndn import ThreadsafeFace 21 | from pyndn.security import KeyChain 22 | # TODO: NFD hack: uncomment once NFD forwarding fixed 23 | # from app.discoveree import Discoveree 24 | # TODO: NFD hack: remove once NFD forwarding fixed 25 | from app.local_discoveree import LocalDiscoveree 26 | from app.cec_tv import CecTv 27 | try: 28 | import asyncio 29 | except ImportError: 30 | import trollius as asyncio 31 | import logging 32 | logging.basicConfig(level=logging.INFO) 33 | 34 | def main(): 35 | loop = asyncio.get_event_loop() 36 | face = ThreadsafeFace(loop, "localhost") 37 | keyChain = KeyChain() 38 | face.setCommandSigningInfo(keyChain, keyChain.getDefaultCertificateName()) 39 | 40 | # TODO: NFD hack: uncomment once NFD forwarding fixed 41 | # discoveree = Discoveree(loop, face, keyChain) 42 | # TODO: NFD hack: remove once NFD forwarding fixed 43 | discoveree = LocalDiscoveree(loop, face, keyChain) 44 | 45 | cecTv = CecTv(loop, face, keyChain, discoveree) 46 | 47 | loop.run_forever() 48 | face.shutdown() 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /examples/hdmi_cec/app/pir_status.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Regents of the University of California. 2 | # Author: Spencer Sutterlin 3 | # 4 | # This file is part of ndn-pi (Named Data Networking - Pi). 5 | # 6 | # ndn-pi is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # A copy of the GNU General Public License is in the file COPYING. 19 | 20 | # TODO: use bisect.insort instead of append 21 | 22 | from pyndn import Exclude 23 | 24 | class PirStatus(object): 25 | def __init__(self): 26 | self._data = [] 27 | self._exclude = Exclude() 28 | 29 | def addData(self, timestamp, value): 30 | if type(timestamp) is not long: 31 | return False 32 | if type(value) is not bool: 33 | return False 34 | if not any(x[0] == timestamp for x in self._data): 35 | self._data.append((timestamp, value)) 36 | return True 37 | else: 38 | return False 39 | 40 | def getExclude(self): 41 | return self._exclude 42 | 43 | def setExcludeUpTo(self, exclude): 44 | self._exclude.clear() 45 | self._exclude.appendAny() 46 | self._exclude.appendComponent(exclude) 47 | 48 | def getLastValue(self): 49 | if len(self._data): 50 | return self._data[-1][1] 51 | else: 52 | return None 53 | 54 | def __repr__(self): 55 | return "PirStatus(data:{0}, exclude:'{1}')".format(self._data, self._exclude.toUri()) 56 | -------------------------------------------------------------------------------- /examples/hdmi_cec/app/remote_device.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Regents of the University of California. 2 | # Author: Spencer Sutterlin 3 | # 4 | # This file is part of ndn-pi (Named Data Networking - Pi). 5 | # 6 | # ndn-pi is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # A copy of the GNU General Public License is in the file COPYING. 19 | 20 | from app.pir_status import PirStatus 21 | from app.cec import CecStatus 22 | 23 | class RemoteDevice(object): 24 | def __init__(self, type, id): 25 | self.type = type 26 | self.id = id 27 | if type == "pir": 28 | self.status = PirStatus() 29 | elif type == "cec": 30 | self.status = CecStatus() 31 | 32 | def __repr__(self): 33 | return "Device(type:{0}, id:{1}, status:{2})".format(self.type, self.id, self.status) 34 | -------------------------------------------------------------------------------- /examples/hdmi_cec/cec_tv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright (C) 2014 Regents of the University of California. 3 | # Author: Spencer Sutterlin 4 | # 5 | # This file is part of ndn-pi (Named Data Networking - Pi). 6 | # 7 | # ndn-pi is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # A copy of the GNU General Public License is in the file COPYING. 20 | 21 | from pyndn import Name, Data 22 | from pyndn.encoding import ProtobufTlv 23 | from util.common import Common 24 | from ndn_pi.iot_node import IotNode 25 | from app.cec import CecDevice, CecCommand 26 | import app.cec_messages_pb2 as pb 27 | import subprocess 28 | import time 29 | 30 | class CecTv(IotNode): 31 | def __init__(self): 32 | super(CecTv, self).__init__() 33 | self.addCommand(Name('sendCommand'), self.onCecCommand, ['cec'], True) 34 | 35 | def processCommands(self, message): 36 | PI = CecDevice.RECORDING_1 37 | # TODO: FORK SEPARATE THREAD FOR THIS 38 | self.log.debug("processCommands: "+ str(message.commands)) 39 | if message.destination == pb.TV: 40 | processedDestination = CecDevice.TV 41 | elif message.destination == pb.RECORDING_1: 42 | processedDestination = CecDevice.RECORDING_1 43 | elif message.destination == pb.PLAYBACK_1: 44 | processedDestination = CecDevice.PLAYBACK_1 45 | elif message.destination == pb.RESERVED_E: 46 | processedDestination = CecDevice.RESERVED_E 47 | elif message.destination == pb.BROADCAST: 48 | processedDestination = CecDevice.BROADCAST 49 | else: 50 | raise RuntimeError("CecDevice/Message not enumerated/implemented") 51 | for command in message.commands: 52 | # TODO: Separate out cec-client call into init (remove -s flag for not single mode anymore) 53 | if command == pb.STANDBY: 54 | cecClient = subprocess.Popen(["cec-client", "-s", "-d", "1"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) 55 | processedCommand = "standby " + format(processedDestination, '01x') 56 | (out, err) = cecClient.communicate(input=processedCommand) 57 | elif command == pb.ON: 58 | cecClient = subprocess.Popen(["cec-client", "-s", "-d", "1"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) 59 | processedCommand = "on " + format(processedDestination, '01x') 60 | (out, err) = cecClient.communicate(input=processedCommand) 61 | elif command == pb.PLAY: 62 | #processedCommand = CecCommand.PLAY 63 | subprocess.check_call(["omxplayer", "-o", "hdmi", "small.mp4"]) 64 | elif command == pb.PAUSE: 65 | cecClient = subprocess.Popen(["cec-client", "-s", "-d", "1"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) 66 | processedCommand = "tx " + format(PI, '01x') + format(processedDestination, '01x') + ":" + CecCommand.PAUSE 67 | (out, err) = cecClient.communicate(input=processedCommand) 68 | elif command == pb.FF: 69 | cecClient = subprocess.Popen(["cec-client", "-s", "-d", "1"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) 70 | processedCommand = "tx " + format(PI, '01x') + format(processedDestination, '01x') + ":" + CecCommand.FF 71 | (out, err) = cecClient.communicate(input=processedCommand) 72 | elif command == pb.RW: 73 | cecClient = subprocess.Popen(["cec-client", "-s", "-d", "1"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) 74 | processedCommand = "tx " + format(PI, '01x') + format(processedDestination, '01x') + ":" + CecCommand.RW 75 | (out, err) = cecClient.communicate(input=processedCommand) 76 | elif command == pb.SEL: 77 | cecClient = subprocess.Popen(["cec-client", "-s", "-d", "1"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) 78 | processedCommand = "tx " + format(PI, '01x') + format(processedDestination, '01x') + ":" + CecCommand.SEL 79 | (out, err) = cecClient.communicate(input=processedCommand) 80 | elif command == pb.UP: 81 | cecClient = subprocess.Popen(["cec-client", "-s", "-d", "1"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) 82 | processedCommand = "tx " + format(PI, '01x') + format(processedDestination, '01x') + ":" + CecCommand.UP 83 | (out, err) = cecClient.communicate(input=processedCommand) 84 | elif command == pb.DOWN: 85 | cecClient = subprocess.Popen(["cec-client", "-s", "-d", "1"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) 86 | processedCommand = "tx " + format(PI, '01x') + format(processedDestination, '01x') + ":" + CecCommand.DOWN 87 | (out, err) = cecClient.communicate(input=processedCommand) 88 | elif command == pb.LEFT: 89 | cecClient = subprocess.Popen(["cec-client", "-s", "-d", "1"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) 90 | processedCommand = "tx " + format(PI, '01x') + format(processedDestination, '01x') + ":" + CecCommand.LEFT 91 | (out, err) = cecClient.communicate(input=processedCommand) 92 | elif command == pb.RIGHT: 93 | cecClient = subprocess.Popen(["cec-client", "-s", "-d", "1"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) 94 | processedCommand = "tx " + format(PI, '01x') + format(processedDestination, '01x') + ":" + CecCommand.RIGHT 95 | (out, err) = cecClient.communicate(input=processedCommand) 96 | elif command == pb.TVMENU: 97 | cecClient = subprocess.Popen(["cec-client", "-s", "-d", "1"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) 98 | processedCommand = "tx " + format(PI, '01x') + format(processedDestination, '01x') + ":" + CecCommand.TVMENU 99 | (out, err) = cecClient.communicate(input=processedCommand) 100 | elif command == pb.DVDMENU: 101 | cecClient = subprocess.Popen(["cec-client", "-s", "-d", "1"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) 102 | processedCommand = "tx " + format(PI, '01x') + format(processedDestination, '01x') + ":" + CecCommand.DVDMENU 103 | (out, err) = cecClient.communicate(input=processedCommand) 104 | elif command == pb.AS: 105 | cecClient = subprocess.Popen(["cec-client", "-s", "-d", "1"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) 106 | processedCommand = "as" 107 | (out, err) = cecClient.communicate(input=processedCommand) 108 | elif command is pb.SLEEP: 109 | time.sleep(1.15) 110 | 111 | def onCecCommand(self, interest): 112 | self.log.debug("Received CEC command") 113 | # check command interest name 114 | # verify command interest 115 | message = pb.CommandMessage() 116 | ProtobufTlv.decode(message, interest.getName().get(3).getValue()) 117 | self.loop.call_soon(self.processCommands, message) 118 | 119 | data = Data(interest.getName()) 120 | data.setContent('ACK') 121 | return data 122 | 123 | if __name__ == '__main__': 124 | node = CecTv() 125 | node.start() 126 | -------------------------------------------------------------------------------- /examples/hdmi_cec/consumer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright (C) 2014 Regents of the University of California. 3 | # Author: Spencer Sutterlin 4 | # Modified by: Adeola Bannis 5 | # 6 | # This file is part of ndn-pi (Named Data Networking - Pi). 7 | # 8 | # ndn-pi is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program. If not, see . 20 | # A copy of the GNU General Public License is in the file COPYING. 21 | 22 | from pyndn import Name 23 | from pyndn import Interest 24 | from pyndn import Exclude 25 | from pyndn.encoding import ProtobufTlv 26 | from ndn_pi.iot_node import IotNode 27 | # This include is produced by: 28 | # protoc --python_out=. cec_messages.proto 29 | import app.cec_messages_pb2 as pb 30 | from app.remote_device import RemoteDevice 31 | import json 32 | 33 | class Consumer(IotNode): 34 | def __init__(self): 35 | super(Consumer, self).__init__() 36 | self._countExpressedInterests = 0 37 | self._callbackCountData = 0 38 | self._callbackCountUniqueData = 0 39 | self._callbackCountTimeout = 0 40 | self._deviceList = [] 41 | 42 | 43 | def setupComplete(self): 44 | #fetch the pir list from the controller 45 | #once we have at least one pir, we can issue the interests 46 | self.loop.call_soon(self.requestDeviceList) 47 | self.loop.call_soon(self.expressInterestPirAndRepeat) 48 | 49 | def getPirs(self): 50 | return [ x for x in self._deviceList if x.type == "pir" ] 51 | 52 | def getPir(self, pirId): 53 | return next((x for x in self._deviceList if x.type == "pir" and x.id == pirId), None) 54 | 55 | def getCecs(self): 56 | return [ x for x in self._deviceList if x.type == "cec" ] 57 | 58 | def getCec(self, cecId): 59 | return next((x for x in self._deviceList if x.type == "cec" and x.id == cecId), None) 60 | 61 | def requestDeviceList(self): 62 | # do this periodically, like every 5 seconds 63 | interestName = Name(self._policyManager.getTrustRootIdentity()).append('listDevices') 64 | self.face.expressInterest(interestName, self.onDataPirList, self.onPirListTimeout) 65 | 66 | def onDataPirList(self, interest, data): 67 | payload = json.loads(data.getContent().toRawStr()) 68 | self.log.debug(str(payload)) 69 | newDeviceList = [] 70 | 71 | try: 72 | cecList = payload["cec"] 73 | except KeyError: 74 | cecList = [] 75 | 76 | for cec in cecList: 77 | existingCec = self.getCec(cec["name"]) 78 | if existingCec is not None: 79 | newDeviceList.append(existingCec) 80 | else: 81 | newDeviceList.append(RemoteDevice("cec", cec["name"])) 82 | 83 | try: 84 | pirList = payload["pir"] 85 | except KeyError: 86 | pirList = [] 87 | 88 | for pir in pirList: 89 | existingPir = self.getPir(pir["name"]) 90 | if existingPir is not None: 91 | newDeviceList.append(existingPir) 92 | else: 93 | newDeviceList.append(RemoteDevice("pir", pir["name"])) 94 | 95 | self._deviceList = newDeviceList 96 | 97 | self.loop.call_later(5, self.requestDeviceList) 98 | 99 | def onPirListTimeout(self, interest): 100 | #try again later 101 | self.loop.call_later(15, self.requestDeviceList) 102 | 103 | def findDeviceIdMatching(self, matchPrefix): 104 | for d in self._deviceList: 105 | devName = Name(d.id) 106 | if devName.match(matchPrefix): 107 | return devName.toUri() 108 | return None 109 | 110 | # Pir Consumption 111 | def onDataPir(self, interest, data): 112 | self._callbackCountData += 1 113 | debugStr = "Got data: " + data.getName().toUri() 114 | debugStr += "\tContent: " + data.getContent().toRawStr() 115 | self.log.debug(debugStr) 116 | 117 | # Extract info from data packet 118 | payload = json.loads(data.getContent().toRawStr()) 119 | pirId = self.findDeviceIdMatching(data.getName()) 120 | timeComponent = data.getName().get(-1) 121 | timestamp = int(timeComponent.toEscapedString()) 122 | pirVal = payload["pir"] 123 | 124 | # Update pirStatus information: add data, exclude last received timestamp 125 | pir = self.getPir(pirId) 126 | pir.status.setExcludeUpTo(timeComponent) 127 | if pir.status.addData(timestamp, pirVal): 128 | self._callbackCountUniqueData += 1 129 | 130 | self.log.info("pir " + str(pirId) + " " + str(pirVal) + " at " + str(timestamp)) 131 | self.controlTV() 132 | 133 | def onTimeoutPir(self, interest): 134 | self._callbackCountTimeout += 1 135 | self.log.debug("Timeout interest: " + interest.getName().toUri()) 136 | 137 | def expressInterestPirAndRepeat(self): 138 | self.log.debug("callbackCountUniqueData: " + str(self._callbackCountUniqueData) + ", callbackCountTimeout: " + str(self._callbackCountTimeout)) 139 | 140 | # Express interest for each pir we have discovered 141 | for pir in self.getPirs(): 142 | interest = Interest(Name(pir.id)) 143 | interest.setExclude(pir.status.getExclude()) 144 | interest.setInterestLifetimeMilliseconds(1000.0) 145 | interest.setChildSelector(1) 146 | 147 | self.face.expressInterest(interest, self.onDataPir, self.onTimeoutPir) 148 | self._countExpressedInterests += 1 149 | debugStr = "Sent interest: " + interest.getName().toUri() 150 | debugStr += "\tExclude: " + interest.getExclude().toUri() 151 | debugStr += "\tLifetime: " + str(interest.getInterestLifetimeMilliseconds()) 152 | 153 | self.log.debug(debugStr) 154 | # Reschedule again in 0.5 sec 155 | self.loop.call_later(1.0, self.expressInterestPirAndRepeat) 156 | 157 | # Cec Control 158 | def onDataCec(self, interest, data): 159 | print "onDataCec" 160 | 161 | def onTimeoutCec(self, interest): 162 | print "onTimeoutCec" 163 | 164 | def controlTV(self): 165 | count = 0 166 | for pir in self.getPirs(): 167 | if pir.status.getLastValue(): 168 | count += 1 169 | if count >= 2: 170 | # TODO: Send command interest to TV 171 | self.log.info("turn on tv") 172 | for cec in self.getCecs(): 173 | message = pb.CommandMessage() 174 | message.destination = pb.TV 175 | message.commands.append(pb.AS) 176 | message.commands.append(pb.SLEEP) 177 | message.commands.append(pb.PLAY) 178 | encodedMessage = ProtobufTlv.encode(message) 179 | interest = Interest(Name(cec.id).append(encodedMessage)) 180 | # self.face.makeCommandInterest(interest) 181 | self.face.expressInterest(interest, self.onDataCec, self.onTimeoutCec) 182 | elif count == 0: 183 | # TODO: Send command interest to TV 184 | self.log.info("turn off tv") 185 | for cec in self.getCecs(): 186 | message = pb.CommandMessage() 187 | message.destination = pb.TV 188 | message.commands.append(pb.STANDBY) 189 | encodedMessage = ProtobufTlv.encode(message) 190 | interest = Interest(Name(cec.id).append(encodedMessage)) 191 | # self.face.makeCommandInterest(interest) 192 | self.face.expressInterest(interest, self.onDataCec, self.onTimeoutCec) 193 | 194 | if __name__ == '__main__': 195 | n = Consumer() 196 | n.start() 197 | -------------------------------------------------------------------------------- /examples/hdmi_cec/pir_publisher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright (C) 2014 Regents of the University of California. 3 | # Author: Spencer Sutterlin 4 | # 5 | # This file is part of ndn-pi (Named Data Networking - Pi). 6 | # 7 | # ndn-pi is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # A copy of the GNU General Public License is in the file COPYING. 20 | 21 | from pyndn import Name 22 | from pyndn import Data 23 | from sensors.pir import Pir 24 | from util.common import Common 25 | import time 26 | import json 27 | try: 28 | import asyncio 29 | except ImportError: 30 | import trollius as asyncio 31 | 32 | from ndn_pi.iot_node import IotNode 33 | import logging 34 | 35 | class PirPublisher(IotNode): 36 | def __init__(self): 37 | super(PirPublisher, self).__init__() 38 | 39 | # find the pins to set up from the config 40 | pinList = [18, 25] 41 | 42 | self._pirs = {} 43 | for pin in pinList: 44 | readCommand = Name('read').append(str(pin)) 45 | self.addCommand(readCommand, self.onReadPir, ['pir'], False) 46 | pir = Pir(pin) 47 | self._pirs[pin] = {"device":pir, "lastVal":pir.read(), "lastTime":int(time.time()*1000)} 48 | 49 | self._count = 0 50 | 51 | def onReadPir(self, interest): 52 | # try to find a matching pir 53 | pirInfo = next((pair[1] for pair in self._pirs.items() 54 | if Name(pair[1]["device"]).match(interest.getName())), None) 55 | 56 | if pirInfo is None: 57 | data = Data(interest.getName()) 58 | data.setContent("MALFORMED COMMAND") 59 | data.getMetaInfo().setFreshnessPeriod(1000) # 1 second, in milliseconds 60 | return data 61 | 62 | lastTime = pirInfo["lastTime"] 63 | lastValue = pirInfo["lastVal"] 64 | 65 | # If interest exclude doesn't match timestamp from last tx'ed data 66 | # then resend data 67 | if not interest.getExclude().matches(Name.Component(str(lastTime))): 68 | print "Received interest without exclude ACK:", interest.getExclude().toUri() 69 | print "\tprevious timestamp:", str(lastTime) 70 | 71 | data = Data(Name(interest.getName()).append(str(lastTime))) 72 | payload = { "pir" : lastValue} 73 | content = json.dumps(payload) 74 | data.setContent(content) 75 | 76 | data.getMetaInfo().setFreshnessPeriod(1000) # 1 second, in milliseconds 77 | 78 | print "Sent data:", data.getName().toUri(), "with content", content 79 | return data 80 | 81 | # otherwise, make new data 82 | currentValue = pirInfo["device"].read() 83 | timestamp = int(time.time() * 1000) # in milliseconds 84 | pirInfo["lastTime"] = timestamp 85 | pirInfo["lastVal"] = currentValue 86 | 87 | data = Data(Name(interest.getName()).append(str(timestamp))) 88 | 89 | payload = { "pir" : currentValue} 90 | content = json.dumps(payload) 91 | data.setContent(content) 92 | 93 | data.getMetaInfo().setFreshnessPeriod(1000) # 1 second, in milliseconds 94 | 95 | print "Sent data:", data.getName().toUri(), "with content", content 96 | return data 97 | 98 | 99 | if __name__ == '__main__': 100 | node = PirPublisher() 101 | node.start() 102 | -------------------------------------------------------------------------------- /examples/hdmi_cec/sensors/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Regents of the University of California. 2 | # Author: Spencer Sutterlin 3 | # 4 | # This file is part of ndn-pi (Named Data Networking - Pi). 5 | # 6 | # ndn-pi is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # A copy of the GNU General Public License is in the file COPYING. 19 | 20 | -------------------------------------------------------------------------------- /examples/hdmi_cec/sensors/fake_pir.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Regents of the University of California. 2 | # Author: Spencer Sutterlin 3 | # 4 | # This file is part of ndn-pi (Named Data Networking - Pi). 5 | # 6 | # ndn-pi is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # A copy of the GNU General Public License is in the file COPYING. 19 | 20 | import random 21 | from time import sleep 22 | 23 | class FakePir(object): 24 | def __init__(self, pin): 25 | self._pin = pin 26 | self._prevVal = False 27 | 28 | def getPin(self): 29 | return self._pin 30 | 31 | def read(self): 32 | if random.random() < 0.1: 33 | self._prevVal = not self._prevVal 34 | return self._prevVal 35 | 36 | if __name__ == "__main__": 37 | pir = FakePir(12) 38 | while True: 39 | v = pir.read() 40 | print v 41 | sleep(1) 42 | -------------------------------------------------------------------------------- /examples/hdmi_cec/sensors/led.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Regents of the University of California. 2 | # Author: Spencer Sutterlin 3 | # 4 | # This file is part of ndn-pi (Named Data Networking - Pi). 5 | # 6 | # ndn-pi is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # A copy of the GNU General Public License is in the file COPYING. 19 | 20 | import RPi.GPIO as gpio 21 | from time import sleep 22 | 23 | class Led(object): 24 | def __init__(self, pin): 25 | self._pin = pin 26 | gpio.setmode(gpio.BOARD) 27 | gpio.setup(self._pin, gpio.OUT) 28 | 29 | def set(self, val): 30 | gpio.output(self._pin, val) 31 | 32 | if __name__ == "__main__": 33 | led = Led(11) 34 | led.set(False) 35 | sleep(2) 36 | led.set(True) 37 | sleep(2) 38 | led.set(False) 39 | -------------------------------------------------------------------------------- /examples/hdmi_cec/sensors/pir.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Regents of the University of California. 2 | # Author: Spencer Sutterlin 3 | # 4 | # This file is part of ndn-pi (Named Data Networking - Pi). 5 | # 6 | # ndn-pi is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # A copy of the GNU General Public License is in the file COPYING. 19 | 20 | import RPi.GPIO as gpio 21 | from time import sleep 22 | 23 | class Pir(object): 24 | def __init__(self, pin): 25 | self._pin = pin 26 | gpio.setmode(gpio.BCM) 27 | gpio.setup(self._pin, gpio.IN) 28 | 29 | def getPin(self): 30 | return self._pin 31 | 32 | def read(self): 33 | return bool(gpio.input(self._pin)) 34 | 35 | if __name__ == "__main__": 36 | pir = Pir(18) 37 | while True: 38 | v = pir.read() 39 | print v 40 | sleep(1) 41 | -------------------------------------------------------------------------------- /examples/hdmi_cec/small.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remap/ndn-pi/8236cecc964ebef275e260a790a0e74583940f8f/examples/hdmi_cec/small.mp4 -------------------------------------------------------------------------------- /examples/hdmi_cec/util/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Regents of the University of California. 2 | # Author: Spencer Sutterlin 3 | # 4 | # This file is part of ndn-pi (Named Data Networking - Pi). 5 | # 6 | # ndn-pi is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # A copy of the GNU General Public License is in the file COPYING. 19 | 20 | -------------------------------------------------------------------------------- /examples/hdmi_cec/util/common.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Regents of the University of California. 2 | # Author: Spencer Sutterlin 3 | # 4 | # This file is part of ndn-pi (Named Data Networking - Pi). 5 | # 6 | # ndn-pi is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # A copy of the GNU General Public License is in the file COPYING. 19 | 20 | class Common(object): 21 | @staticmethod 22 | def getSerial(): 23 | with open('/proc/cpuinfo') as f: 24 | for line in f: 25 | if line.startswith('Serial'): 26 | return line.split(':')[1].strip() 27 | -------------------------------------------------------------------------------- /examples/led_control/README.md: -------------------------------------------------------------------------------- 1 | LED Control Network 2 | =================== 3 | 4 | 5 | This example demonstrates three node types that make up a simple IoT network. 6 | 7 | Node Types 8 | ---------- 9 | 10 | ###Controller 11 | 12 | The controller in this network has no special function: it listens for certificate requests and device listing requests. 13 | 14 | ###LED Node 15 | 16 | This node directly controls an LED attached to a GPIO pin (by default, 24). 17 | 18 | ### Multi-LED Node 19 | This is a more customizable version of the LED node. It separately controls LEDs attached to the GPIO pins 20 | (by default, 17 and 24). 21 | 22 | ###User Node 23 | This node periodically asks the controller for a device listing, and randomly sends an available LED command. 24 | 25 | Setup 26 | ------- 27 | 28 | ### LED Node 29 | This node expects to have an LED connected to GPIO pin 24 by default, but you can change the pin number by 30 | specifying a different number on the command line. 31 | 32 | 33 | 34 | ### Multi-LED Node 35 | This node expects to have LEDs connected to GPIO pins 17 and 24 by default. 36 | 37 | 38 | 39 | ### Network Setup 40 | See the top-level README.md in ndn-pi for NDN network setup steps. 41 | 42 | Running the Example 43 | ------------------- 44 | The nodes can be started in any order, but the controller must be running to set up new nodes, using the 'P' option in the controller menu. 45 | You can also use the 'D' option to see a listing of all commands available to the network. 46 | 47 | **Note**: If the controller goes down, nodes may keep running but will not be able to refresh their directories. Also, when the controller is 48 | restarted, it may take a while to receive updates from already running nodes. 49 | 50 | -------------------------------------------------------------------------------- /examples/led_control/led_multi_node.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */ 3 | # 4 | # Copyright (C) 2014 Regents of the University of California. 5 | # Author: Adeola Bannis 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # A copy of the GNU General Public License is in the file COPYING. 20 | 21 | from __future__ import print_function 22 | from ndn_pi.iot_node import IotNode 23 | from pyndn import Name, Data, Interest 24 | import RPi.GPIO as GPIO 25 | 26 | class LedNode(IotNode): 27 | 28 | def __init__(self): 29 | GPIO.setmode(GPIO.BCM) 30 | super(LedNode, self).__init__() 31 | # set up our pin commands 32 | 33 | self.pinList = [24, 17] 34 | for pinNumber in self.pinList: 35 | GPIO.setup(pinNumber, GPIO.OUT) 36 | onCommand = Name('setLight').append(str(pinNumber)).append('on') 37 | offCommand = Name('setLight').append(str(pinNumber)).append('off') 38 | self.addCommand(onCommand, self.onLightCommand, 39 | ['led', 'light'], False) 40 | self.addCommand(offCommand, self.onLightCommand, 41 | ['led', 'light'], False) 42 | 43 | def onLightCommand(self, interest): 44 | response = Data(interest.getName()) 45 | # commands are .../setLight/pinNumber/[on|off] 46 | interestName = interest.getName() 47 | try: 48 | pinNumber = int(interestName.get(self.prefix.size()+1).toEscapedString()) 49 | if pinNumber not in self.pinList: 50 | raise RuntimeError('Bad pin number') 51 | commandTypeComponent = interest.getName().get(self.prefix.size()+2) 52 | commandType = commandTypeComponent.toEscapedString() 53 | if commandType == 'on': 54 | GPIO.output(pinNumber, GPIO.HIGH) 55 | elif commandType == 'off': 56 | GPIO.output(pinNumber, GPIO.LOW) 57 | else: 58 | raise RuntimeError('BadCommand') 59 | response.setContent('ACK') 60 | except (IndexError, RuntimeError, ValueError): 61 | #malformed interest 62 | response.setContent('NACK') 63 | return response 64 | 65 | if __name__ == '__main__': 66 | node = LedNode() 67 | node.start() 68 | -------------------------------------------------------------------------------- /examples/led_control/led_node.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */ 3 | # 4 | # Copyright (C) 2014 Regents of the University of California. 5 | # Author: Adeola Bannis 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # A copy of the GNU General Public License is in the file COPYING. 20 | 21 | from __future__ import print_function 22 | from ndn_pi.iot_node import IotNode 23 | from pyndn import Name, Data, Interest 24 | import RPi.GPIO as GPIO 25 | 26 | class LedNode(IotNode): 27 | def __init__(self, pinNumber): 28 | super(LedNode, self).__init__() 29 | 30 | # can we tell if the pin number is invalid? 31 | self.pinNumber = pinNumber 32 | GPIO.setmode(GPIO.BCM) 33 | GPIO.setup(pinNumber, GPIO.OUT) 34 | 35 | onCommand = Name('setLight').append('on') 36 | offCommand = Name('setLight').append('off') 37 | self.addCommand(onCommand, self.onLightCommand, ['led', 'light'], 38 | False) 39 | self.addCommand(offCommand, self.onLightCommand, ['led', 'light'], 40 | False) 41 | 42 | self.blinkPWM = None 43 | 44 | def onLightCommand(self, interest): 45 | self.log.debug("Light command") 46 | response = Data(interest.getName()) 47 | try: 48 | commandTypeComponent = interest.getName().get(self.prefix.size()+1) 49 | commandType = commandTypeComponent.toEscapedString() 50 | if commandType == 'on': 51 | GPIO.output(self.pinNumber, GPIO.HIGH) 52 | elif commandType == 'off': 53 | GPIO.output(self.pinNumber, GPIO.LOW) 54 | else: 55 | raise RuntimeError('BadCommand') 56 | response.setContent('ACK') 57 | except IndexError, RuntimeError: 58 | #malformed interest 59 | response.setContent('NACK') 60 | return response 61 | 62 | def onBlinkCommand(self, interest): 63 | self.log.debug("Blink command") 64 | response = Data(interest.getName()) 65 | if self.blinkPWM is None: 66 | self.blinkPWM = GPIO.PWM(self.pinNumber, 2.0) 67 | self.blinkPWM.start(50) 68 | else: 69 | self.blinkPWM.stop() 70 | self.blinkPWM = None 71 | response.setContent('ACK') 72 | return response 73 | 74 | if __name__ == '__main__': 75 | import sys 76 | try: 77 | pinNumber = int(sys.argv[1]) 78 | except IndexError, ValueError: 79 | pinNumber = 24 80 | node = LedNode(pinNumber) 81 | node.start() 82 | -------------------------------------------------------------------------------- /examples/led_control/led_user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */ 3 | # 4 | # Copyright (C) 2014 Regents of the University of California. 5 | # Author: Adeola Bannis 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # A copy of the GNU General Public License is in the file COPYING. 20 | 21 | from __future__ import print_function 22 | from ndn_pi.iot_node import IotNode 23 | from pyndn import Name, Data, Interest 24 | import json 25 | import random 26 | 27 | class LedUserNode(IotNode): 28 | def __init__(self): 29 | super(LedUserNode, self).__init__() 30 | self._ledCommands = [] 31 | 32 | def setupComplete(self): 33 | self.loop.call_soon(self.requestDeviceList) 34 | self.loop.call_later(1, self.sendRandomCommand) 35 | 36 | def onListReceived(self, interest, data): 37 | # the list is send at json 38 | deviceDict = json.loads(data.getContent().toRawStr()) 39 | try: 40 | ledCommands = deviceDict['led'] 41 | self._ledCommands = [info['name'] for info in ledCommands] 42 | except (IndexError, KeyError, TypeError): 43 | self.log.debug('Did not find LED commands') 44 | finally: 45 | self.loop.call_later(5, self.requestDeviceList) 46 | 47 | def onCommandAck(self, interest, data): 48 | pass 49 | 50 | def onCommandTimeout(self, interest): 51 | pass 52 | 53 | def sendRandomCommand(self): 54 | try: 55 | chosenCommand = random.choice(self._ledCommands) 56 | interest = Interest(Name(chosenCommand)) 57 | self.log.debug('Sending command {}'.format(chosenCommand)) 58 | # uncomment the following line to sign interests (slower than unsigned) 59 | #self.face.makeCommandInterest(interest) 60 | self.face.expressInterest(interest, self.onCommandAck, self.onCommandTimeout) 61 | except IndexError: 62 | pass 63 | finally: 64 | self.loop.call_later(1, self.sendRandomCommand) 65 | 66 | def onListTimeout(self, interest): 67 | self.log.debug('Timed out asking for device list') 68 | self.loop.call_later(15, self.requestDeviceList) 69 | 70 | def requestDeviceList(self): 71 | commandName = self._policyManager.getTrustRootIdentity().append('listDevices') 72 | interest = Interest(commandName) 73 | self.face.makeCommandInterest(interest) 74 | 75 | self.face.expressInterest(interest, self.onListReceived, self.onListTimeout) 76 | 77 | 78 | if __name__ == '__main__': 79 | import logging 80 | node = LedUserNode() 81 | node.start() 82 | -------------------------------------------------------------------------------- /iot_controller.sample: -------------------------------------------------------------------------------- 1 | device { 2 | environmentPrefix /home 3 | controllerName gateway 4 | } 5 | -------------------------------------------------------------------------------- /ndn-iot-console: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python -m ndn_pi.iot_console 3 | -------------------------------------------------------------------------------- /ndn-iot-controller: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python -m ndn_pi.iot_controller $@ 3 | -------------------------------------------------------------------------------- /ndn-iot-start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SHOW_USAGE=0 4 | n=0 5 | while getopts ":h" opt; do 6 | case $opt in 7 | h) 8 | SHOW_USAGE=1 9 | ;; 10 | \?) 11 | SHOW_USAGE=1 12 | ;; 13 | :) 14 | SHOW_USAGE=1 15 | ;; 16 | esac 17 | done 18 | 19 | [[ $SHOW_USAGE -eq 1 ]] && { echo "Usage: $0 [interface ...]"; exit 1; } 20 | 21 | 22 | if [ "$#" -eq 0 ]; then 23 | INTERFACES=( "wlan0" ) 24 | else 25 | INTERFACES=( "$@" ) 26 | fi 27 | 28 | nfd-stop 29 | nfd-start 30 | 31 | nfd-status -c &> /dev/null 32 | 33 | [[ "$?" -ne 0 ]] && { echo "NFD could not be started, aborting"; exit 1;} 34 | 35 | for INTERFACE in "${INTERFACES[@]}" 36 | do 37 | echo "Registering NFD on interface $INTERFACE..." 38 | IP_ADDR=`ifconfig $INTERFACE | grep -Eo 'inet addr:[0-9\.]*'| cut -d: -f2` 39 | if [ $IP_ADDR ]; then 40 | FACE_ID=`nfd-status -f | grep $IP_ADDR | awk '{print $1}' | cut -d= -f2` 41 | nfdc register / $FACE_ID 42 | else 43 | echo "$INTERFACE appears down, skipping..." 44 | fi 45 | done 46 | 47 | -------------------------------------------------------------------------------- /ndn-wifi-passwd: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PASS1="" 4 | PASS_MATCH=0 5 | 6 | 7 | updatePasswd() { 8 | newpass=$1 9 | pass_success=0 10 | 11 | tempdir=$(mktemp -dt) && 12 | sed -re "s/wpa_passphrase=.+/wpa_passphrase=$newpass/" /etc/hostapd/hostapd.conf > "$tempdir/hostapd.conf" && 13 | sed -re "/ssid=\"Raspi_NDN\"/,+5{s/psk=.+$/psk=\"$newpass\"/}" /etc/wpa_supplicant/wpa_supplicant.conf > "$tempdir/wpa_supplicant.conf" && 14 | sudo cp /etc/hostapd/hostapd.conf /etc/hostapd/hostapd.conf.bak && 15 | sudo cp /etc/wpa_supplicant/wpa_supplicant.conf /etc/wpa_supplicant/wpa_supplicant.conf.bak && 16 | sudo mv "$tempdir/hostapd.conf" /etc/hostapd/hostapd.conf && 17 | sudo mv "$tempdir/wpa_supplicant.conf" /etc/wpa_supplicant/wpa_supplicant.conf && 18 | pass_success=1 && 19 | echo "Password updated." 20 | } 21 | 22 | while [ "$PASS_MATCH" -eq 0 ]; do 23 | while [ -z "$PASS1" ]; do 24 | read -r -s -p "Enter new wifi password: " PASS1 25 | done 26 | echo 27 | read -r -s -p "Confirm new password: " PASS2 28 | echo 29 | echo 30 | if [ "$PASS1" = "$PASS2" ]; then 31 | PASS_MATCH=1 32 | updatePasswd $PASS1 33 | if [ "$pass_success" -eq 0 ]; then 34 | echo "Could not update configuration files" 35 | exit 1 36 | else 37 | service hostapd status 38 | if [[ $? -eq 0 ]]; then 39 | echo "Restarting hostapd" 40 | sudo service hostapd restart 41 | else 42 | echo "Restarting wpa_supplicant" 43 | sudo ifconfig wlan0 down 44 | sudo dhclient -r wlan0 45 | sudo killall wpa_supplicant 46 | sudo wpa_supplicant -dd -iwlan0 -c /etc/wpa_supplicant/wpa_supplicant.conf 2>&1 | logger -t wpa_supplicant & 47 | sudo ifconfig wlan0 up 48 | sudo dhclient -1 wlan0 49 | fi 50 | fi 51 | else 52 | echo "Passwords do not match." 53 | PASS1="" 54 | fi 55 | done 56 | 57 | 58 | -------------------------------------------------------------------------------- /ndn_pi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remap/ndn-pi/8236cecc964ebef275e260a790a0e74583940f8f/ndn_pi/__init__.py -------------------------------------------------------------------------------- /ndn_pi/base_node.py: -------------------------------------------------------------------------------- 1 | 2 | # -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */ 3 | # 4 | # Copyright (C) 2014 Regents of the University of California. 5 | # Author: Adeola Bannis 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # A copy of the GNU General Public License is in the file COPYING. 20 | import logging 21 | import time 22 | import sys 23 | import random 24 | 25 | from pyndn import Name, Face, Interest, Data, ThreadsafeFace 26 | from pyndn.security import KeyChain 27 | from pyndn.security.policy import ConfigPolicyManager 28 | from pyndn.security.identity import IdentityManager 29 | from pyndn.security.security_exception import SecurityException 30 | 31 | from ndn_pi.security import IotIdentityStorage,IotPolicyManager, IotIdentityManager 32 | 33 | from collections import namedtuple 34 | 35 | try: 36 | import asyncio 37 | except ImportError: 38 | import trollius as asyncio 39 | 40 | Command = namedtuple('Command', ['suffix', 'function', 'keywords', 'isSigned']) 41 | 42 | class BaseNode(object): 43 | """ 44 | This class contains methods/attributes common to both node and controller. 45 | 46 | """ 47 | def __init__(self): 48 | """ 49 | Initialize the network and security classes for the node 50 | """ 51 | super(BaseNode, self).__init__() 52 | 53 | self._identityStorage = IotIdentityStorage() 54 | self._identityManager = IotIdentityManager(self._identityStorage) 55 | self._policyManager = IotPolicyManager(self._identityStorage) 56 | 57 | # hopefully there is some private/public key pair available 58 | self._keyChain = KeyChain(self._identityManager, self._policyManager) 59 | 60 | self._registrationFailures = 0 61 | self._prepareLogging() 62 | 63 | self._setupComplete = False 64 | self._instanceSerial = None 65 | 66 | # waiting devices register this prefix and respond to discovery 67 | # or configuration interest 68 | self._hubPrefix = Name('/localhop/configure') 69 | 70 | def getSerial(self): 71 | """ 72 | Since you may wish to run two nodes on a Raspberry Pi, each 73 | node will generate a unique serial number each time it starts up. 74 | """ 75 | if self._instanceSerial is None: 76 | prefixLen = 4 77 | prefix = '' 78 | for i in range(prefixLen): 79 | prefix += (chr(random.randint(0,0xff))) 80 | suffix = self.getDeviceSerial().lstrip('0') 81 | self._instanceSerial = '-'.join([prefix.encode('hex'), suffix]) 82 | return self._instanceSerial 83 | 84 | ## 85 | # Logging 86 | ## 87 | def _prepareLogging(self): 88 | self.log = logging.getLogger(str(self.__class__)) 89 | self.log.setLevel(logging.DEBUG) 90 | logFormat = "%(asctime)-15s %(name)-20s %(funcName)-20s (%(levelname)-8s):\n\t%(message)s" 91 | self._console = logging.StreamHandler() 92 | self._console.setFormatter(logging.Formatter(logFormat)) 93 | self._console.setLevel(logging.INFO) 94 | # without this, a lot of ThreadsafeFace errors get swallowed up 95 | logging.getLogger("trollius").addHandler(self._console) 96 | self.log.addHandler(self._console) 97 | 98 | def setLogLevel(self, level): 99 | """ 100 | Set the log level that will be output to standard error 101 | :param level: A log level constant defined in the logging module (e.g. logging.INFO) 102 | """ 103 | self._console.setLevel(level) 104 | 105 | def getLogger(self): 106 | """ 107 | :return: The logger associated with this node 108 | :rtype: logging.Logger 109 | """ 110 | return self.log 111 | 112 | ### 113 | # Startup and shutdown 114 | ### 115 | def beforeLoopStart(self): 116 | """ 117 | Called before the event loop starts. 118 | """ 119 | pass 120 | 121 | def getDefaultCertificateName(self): 122 | try: 123 | certName = self._identityStorage.getDefaultCertificateNameForIdentity( 124 | self._policyManager.getDeviceIdentity()) 125 | except SecurityException: 126 | certName = self._keyChain.getDefaultCertificateName() 127 | 128 | return certName 129 | 130 | def start(self): 131 | """ 132 | Begins the event loop. After this, the node's Face is set up and it can 133 | send/receive interests+data 134 | """ 135 | self.log.info("Starting up") 136 | self.loop = asyncio.get_event_loop() 137 | self.face = ThreadsafeFace(self.loop, '') 138 | self.face.setCommandSigningInfo(self._keyChain, self.getDefaultCertificateName()) 139 | self._keyChain.setFace(self.face) 140 | 141 | self._isStopped = False 142 | self.face.stopWhen(lambda:self._isStopped) 143 | self.beforeLoopStart() 144 | 145 | try: 146 | self.loop.run_forever() 147 | except KeyboardInterrupt: 148 | pass 149 | except Exception as e: 150 | self.log.exception(e, exc_info=True) 151 | finally: 152 | self._isStopped = True 153 | 154 | def stop(self): 155 | """ 156 | Stops the node, taking it off the network 157 | """ 158 | self.log.info("Shutting down") 159 | self._isStopped = True 160 | 161 | ### 162 | # Data handling 163 | ### 164 | def signData(self, data): 165 | """ 166 | Sign the data with our network certificate 167 | :param pyndn.Data data: The data to sign 168 | """ 169 | self._keyChain.sign(data, self.getDefaultCertificateName()) 170 | 171 | def sendData(self, data, transport, sign=True): 172 | """ 173 | Reply to an interest with a data packet, optionally signing it. 174 | :param pyndn.Data data: The response data packet 175 | :param pyndn.Transport transport: The transport to send the data through. This is 176 | obtained from an incoming interest handler 177 | :param boolean sign: (optional, default=True) Whether the response must be signed. 178 | """ 179 | if sign: 180 | self.signData(data) 181 | transport.send(data.wireEncode().buf()) 182 | 183 | ### 184 | # 185 | # 186 | ## 187 | def onRegisterFailed(self, prefix): 188 | """ 189 | Called when the node cannot register its name with the forwarder 190 | :param pyndn.Name prefix: The network name that failed registration 191 | """ 192 | if self._registrationFailures < 5: 193 | self._registrationFailures += 1 194 | self.log.warn("Could not register {}, retry: {}/{}".format(prefix.toUri(), self._registrationFailures, 5)) 195 | self.face.registerPrefix(self.prefix, self._onCommandReceived, self.onRegisterFailed) 196 | else: 197 | self.log.critical("Could not register device prefix, ABORTING") 198 | self._isStopped = True 199 | 200 | def verificationFailed(self, dataOrInterest): 201 | """ 202 | Called when verification of a data packet or command interest fails. 203 | :param pyndn.Data or pyndn.Interest: The packet that could not be verified 204 | """ 205 | self.log.info("Received invalid" + dataOrInterest.getName().toUri()) 206 | 207 | @staticmethod 208 | def getDeviceSerial(): 209 | """ 210 | Find and return the serial number of the Raspberry Pi. Provided in case 211 | you wish to distinguish data from nodes with the same name by serial. 212 | :return: The serial number extracted from device information in /proc/cpuinfo 213 | :rtype: str 214 | """ 215 | with open('/proc/cpuinfo') as f: 216 | for line in f: 217 | if line.startswith('Serial'): 218 | return line.split(':')[1].strip() 219 | 220 | -------------------------------------------------------------------------------- /ndn_pi/commands/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['cert_request_pb2', 'configure_device_pb2', 'send_pairing_info_pb2', 'update_capabilities_pb2'] 2 | 3 | from cert_request_pb2 import CertificateRequestMessage 4 | from update_capabilities_pb2 import UpdateCapabilitiesCommandMessage 5 | from configure_device_pb2 import DeviceConfigurationMessage 6 | from send_pairing_info_pb2 import DevicePairingInfoMessage 7 | -------------------------------------------------------------------------------- /ndn_pi/commands/cert-request.proto: -------------------------------------------------------------------------------- 1 | message CertificateRequestMessage { 2 | message Name { 3 | repeated bytes components = 8; 4 | } 5 | message CertificateRequest { 6 | required Name keyName = 220; 7 | required uint32 keyType = 221; 8 | required bytes keyBits = 222; 9 | } 10 | required CertificateRequest command = 223; 11 | } 12 | -------------------------------------------------------------------------------- /ndn_pi/commands/cert_request_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | 3 | from google.protobuf import descriptor 4 | from google.protobuf import message 5 | from google.protobuf import reflection 6 | from google.protobuf import descriptor_pb2 7 | # @@protoc_insertion_point(imports) 8 | 9 | 10 | 11 | DESCRIPTOR = descriptor.FileDescriptor( 12 | name='cert-request.proto', 13 | package='', 14 | serialized_pb='\n\x12\x63\x65rt-request.proto\"\xe5\x01\n\x19\x43\x65rtificateRequestMessage\x12?\n\x07\x63ommand\x18\xdf\x01 \x02(\x0b\x32-.CertificateRequestMessage.CertificateRequest\x1a\x1a\n\x04Name\x12\x12\n\ncomponents\x18\x08 \x03(\x0c\x1ak\n\x12\x43\x65rtificateRequest\x12\x31\n\x07keyName\x18\xdc\x01 \x02(\x0b\x32\x1f.CertificateRequestMessage.Name\x12\x10\n\x07keyType\x18\xdd\x01 \x02(\r\x12\x10\n\x07keyBits\x18\xde\x01 \x02(\x0c') 15 | 16 | 17 | 18 | 19 | _CERTIFICATEREQUESTMESSAGE_NAME = descriptor.Descriptor( 20 | name='Name', 21 | full_name='CertificateRequestMessage.Name', 22 | filename=None, 23 | file=DESCRIPTOR, 24 | containing_type=None, 25 | fields=[ 26 | descriptor.FieldDescriptor( 27 | name='components', full_name='CertificateRequestMessage.Name.components', index=0, 28 | number=8, type=12, cpp_type=9, label=3, 29 | has_default_value=False, default_value=[], 30 | message_type=None, enum_type=None, containing_type=None, 31 | is_extension=False, extension_scope=None, 32 | options=None), 33 | ], 34 | extensions=[ 35 | ], 36 | nested_types=[], 37 | enum_types=[ 38 | ], 39 | options=None, 40 | is_extendable=False, 41 | extension_ranges=[], 42 | serialized_start=117, 43 | serialized_end=143, 44 | ) 45 | 46 | _CERTIFICATEREQUESTMESSAGE_CERTIFICATEREQUEST = descriptor.Descriptor( 47 | name='CertificateRequest', 48 | full_name='CertificateRequestMessage.CertificateRequest', 49 | filename=None, 50 | file=DESCRIPTOR, 51 | containing_type=None, 52 | fields=[ 53 | descriptor.FieldDescriptor( 54 | name='keyName', full_name='CertificateRequestMessage.CertificateRequest.keyName', index=0, 55 | number=220, type=11, cpp_type=10, label=2, 56 | has_default_value=False, default_value=None, 57 | message_type=None, enum_type=None, containing_type=None, 58 | is_extension=False, extension_scope=None, 59 | options=None), 60 | descriptor.FieldDescriptor( 61 | name='keyType', full_name='CertificateRequestMessage.CertificateRequest.keyType', index=1, 62 | number=221, type=13, cpp_type=3, label=2, 63 | has_default_value=False, default_value=0, 64 | message_type=None, enum_type=None, containing_type=None, 65 | is_extension=False, extension_scope=None, 66 | options=None), 67 | descriptor.FieldDescriptor( 68 | name='keyBits', full_name='CertificateRequestMessage.CertificateRequest.keyBits', index=2, 69 | number=222, type=12, cpp_type=9, label=2, 70 | has_default_value=False, default_value="", 71 | message_type=None, enum_type=None, containing_type=None, 72 | is_extension=False, extension_scope=None, 73 | options=None), 74 | ], 75 | extensions=[ 76 | ], 77 | nested_types=[], 78 | enum_types=[ 79 | ], 80 | options=None, 81 | is_extendable=False, 82 | extension_ranges=[], 83 | serialized_start=145, 84 | serialized_end=252, 85 | ) 86 | 87 | _CERTIFICATEREQUESTMESSAGE = descriptor.Descriptor( 88 | name='CertificateRequestMessage', 89 | full_name='CertificateRequestMessage', 90 | filename=None, 91 | file=DESCRIPTOR, 92 | containing_type=None, 93 | fields=[ 94 | descriptor.FieldDescriptor( 95 | name='command', full_name='CertificateRequestMessage.command', index=0, 96 | number=223, type=11, cpp_type=10, label=2, 97 | has_default_value=False, default_value=None, 98 | message_type=None, enum_type=None, containing_type=None, 99 | is_extension=False, extension_scope=None, 100 | options=None), 101 | ], 102 | extensions=[ 103 | ], 104 | nested_types=[_CERTIFICATEREQUESTMESSAGE_NAME, _CERTIFICATEREQUESTMESSAGE_CERTIFICATEREQUEST, ], 105 | enum_types=[ 106 | ], 107 | options=None, 108 | is_extendable=False, 109 | extension_ranges=[], 110 | serialized_start=23, 111 | serialized_end=252, 112 | ) 113 | 114 | _CERTIFICATEREQUESTMESSAGE_NAME.containing_type = _CERTIFICATEREQUESTMESSAGE; 115 | _CERTIFICATEREQUESTMESSAGE_CERTIFICATEREQUEST.fields_by_name['keyName'].message_type = _CERTIFICATEREQUESTMESSAGE_NAME 116 | _CERTIFICATEREQUESTMESSAGE_CERTIFICATEREQUEST.containing_type = _CERTIFICATEREQUESTMESSAGE; 117 | _CERTIFICATEREQUESTMESSAGE.fields_by_name['command'].message_type = _CERTIFICATEREQUESTMESSAGE_CERTIFICATEREQUEST 118 | DESCRIPTOR.message_types_by_name['CertificateRequestMessage'] = _CERTIFICATEREQUESTMESSAGE 119 | 120 | class CertificateRequestMessage(message.Message): 121 | __metaclass__ = reflection.GeneratedProtocolMessageType 122 | 123 | class Name(message.Message): 124 | __metaclass__ = reflection.GeneratedProtocolMessageType 125 | DESCRIPTOR = _CERTIFICATEREQUESTMESSAGE_NAME 126 | 127 | # @@protoc_insertion_point(class_scope:CertificateRequestMessage.Name) 128 | 129 | class CertificateRequest(message.Message): 130 | __metaclass__ = reflection.GeneratedProtocolMessageType 131 | DESCRIPTOR = _CERTIFICATEREQUESTMESSAGE_CERTIFICATEREQUEST 132 | 133 | # @@protoc_insertion_point(class_scope:CertificateRequestMessage.CertificateRequest) 134 | DESCRIPTOR = _CERTIFICATEREQUESTMESSAGE 135 | 136 | # @@protoc_insertion_point(class_scope:CertificateRequestMessage) 137 | 138 | # @@protoc_insertion_point(module_scope) 139 | -------------------------------------------------------------------------------- /ndn_pi/commands/configure-device.proto: -------------------------------------------------------------------------------- 1 | message DeviceConfigurationMessage { 2 | message Name { 3 | repeated bytes components = 8; 4 | } 5 | message DeviceConfiguration { 6 | required Name networkPrefix = 224; 7 | required Name controllerName = 225; 8 | required Name deviceSuffix = 226; 9 | } 10 | required DeviceConfiguration configuration = 227; 11 | } 12 | -------------------------------------------------------------------------------- /ndn_pi/commands/configure_device_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | 3 | from google.protobuf import descriptor 4 | from google.protobuf import message 5 | from google.protobuf import reflection 6 | from google.protobuf import descriptor_pb2 7 | # @@protoc_insertion_point(imports) 8 | 9 | 10 | 11 | DESCRIPTOR = descriptor.FileDescriptor( 12 | name='configure-device.proto', 13 | package='', 14 | serialized_pb='\n\x16\x63onfigure-device.proto\"\xc7\x02\n\x1a\x44\x65viceConfigurationMessage\x12G\n\rconfiguration\x18\xe3\x01 \x02(\x0b\x32/.DeviceConfigurationMessage.DeviceConfiguration\x1a\x1a\n\x04Name\x12\x12\n\ncomponents\x18\x08 \x03(\x0c\x1a\xc3\x01\n\x13\x44\x65viceConfiguration\x12\x38\n\rnetworkPrefix\x18\xe0\x01 \x02(\x0b\x32 .DeviceConfigurationMessage.Name\x12\x39\n\x0e\x63ontrollerName\x18\xe1\x01 \x02(\x0b\x32 .DeviceConfigurationMessage.Name\x12\x37\n\x0c\x64\x65viceSuffix\x18\xe2\x01 \x02(\x0b\x32 .DeviceConfigurationMessage.Name') 15 | 16 | 17 | 18 | 19 | _DEVICECONFIGURATIONMESSAGE_NAME = descriptor.Descriptor( 20 | name='Name', 21 | full_name='DeviceConfigurationMessage.Name', 22 | filename=None, 23 | file=DESCRIPTOR, 24 | containing_type=None, 25 | fields=[ 26 | descriptor.FieldDescriptor( 27 | name='components', full_name='DeviceConfigurationMessage.Name.components', index=0, 28 | number=8, type=12, cpp_type=9, label=3, 29 | has_default_value=False, default_value=[], 30 | message_type=None, enum_type=None, containing_type=None, 31 | is_extension=False, extension_scope=None, 32 | options=None), 33 | ], 34 | extensions=[ 35 | ], 36 | nested_types=[], 37 | enum_types=[ 38 | ], 39 | options=None, 40 | is_extendable=False, 41 | extension_ranges=[], 42 | serialized_start=130, 43 | serialized_end=156, 44 | ) 45 | 46 | _DEVICECONFIGURATIONMESSAGE_DEVICECONFIGURATION = descriptor.Descriptor( 47 | name='DeviceConfiguration', 48 | full_name='DeviceConfigurationMessage.DeviceConfiguration', 49 | filename=None, 50 | file=DESCRIPTOR, 51 | containing_type=None, 52 | fields=[ 53 | descriptor.FieldDescriptor( 54 | name='networkPrefix', full_name='DeviceConfigurationMessage.DeviceConfiguration.networkPrefix', index=0, 55 | number=224, type=11, cpp_type=10, label=2, 56 | has_default_value=False, default_value=None, 57 | message_type=None, enum_type=None, containing_type=None, 58 | is_extension=False, extension_scope=None, 59 | options=None), 60 | descriptor.FieldDescriptor( 61 | name='controllerName', full_name='DeviceConfigurationMessage.DeviceConfiguration.controllerName', index=1, 62 | number=225, type=11, cpp_type=10, label=2, 63 | has_default_value=False, default_value=None, 64 | message_type=None, enum_type=None, containing_type=None, 65 | is_extension=False, extension_scope=None, 66 | options=None), 67 | descriptor.FieldDescriptor( 68 | name='deviceSuffix', full_name='DeviceConfigurationMessage.DeviceConfiguration.deviceSuffix', index=2, 69 | number=226, type=11, cpp_type=10, label=2, 70 | has_default_value=False, default_value=None, 71 | message_type=None, enum_type=None, containing_type=None, 72 | is_extension=False, extension_scope=None, 73 | options=None), 74 | ], 75 | extensions=[ 76 | ], 77 | nested_types=[], 78 | enum_types=[ 79 | ], 80 | options=None, 81 | is_extendable=False, 82 | extension_ranges=[], 83 | serialized_start=159, 84 | serialized_end=354, 85 | ) 86 | 87 | _DEVICECONFIGURATIONMESSAGE = descriptor.Descriptor( 88 | name='DeviceConfigurationMessage', 89 | full_name='DeviceConfigurationMessage', 90 | filename=None, 91 | file=DESCRIPTOR, 92 | containing_type=None, 93 | fields=[ 94 | descriptor.FieldDescriptor( 95 | name='configuration', full_name='DeviceConfigurationMessage.configuration', index=0, 96 | number=227, type=11, cpp_type=10, label=2, 97 | has_default_value=False, default_value=None, 98 | message_type=None, enum_type=None, containing_type=None, 99 | is_extension=False, extension_scope=None, 100 | options=None), 101 | ], 102 | extensions=[ 103 | ], 104 | nested_types=[_DEVICECONFIGURATIONMESSAGE_NAME, _DEVICECONFIGURATIONMESSAGE_DEVICECONFIGURATION, ], 105 | enum_types=[ 106 | ], 107 | options=None, 108 | is_extendable=False, 109 | extension_ranges=[], 110 | serialized_start=27, 111 | serialized_end=354, 112 | ) 113 | 114 | _DEVICECONFIGURATIONMESSAGE_NAME.containing_type = _DEVICECONFIGURATIONMESSAGE; 115 | _DEVICECONFIGURATIONMESSAGE_DEVICECONFIGURATION.fields_by_name['networkPrefix'].message_type = _DEVICECONFIGURATIONMESSAGE_NAME 116 | _DEVICECONFIGURATIONMESSAGE_DEVICECONFIGURATION.fields_by_name['controllerName'].message_type = _DEVICECONFIGURATIONMESSAGE_NAME 117 | _DEVICECONFIGURATIONMESSAGE_DEVICECONFIGURATION.fields_by_name['deviceSuffix'].message_type = _DEVICECONFIGURATIONMESSAGE_NAME 118 | _DEVICECONFIGURATIONMESSAGE_DEVICECONFIGURATION.containing_type = _DEVICECONFIGURATIONMESSAGE; 119 | _DEVICECONFIGURATIONMESSAGE.fields_by_name['configuration'].message_type = _DEVICECONFIGURATIONMESSAGE_DEVICECONFIGURATION 120 | DESCRIPTOR.message_types_by_name['DeviceConfigurationMessage'] = _DEVICECONFIGURATIONMESSAGE 121 | 122 | class DeviceConfigurationMessage(message.Message): 123 | __metaclass__ = reflection.GeneratedProtocolMessageType 124 | 125 | class Name(message.Message): 126 | __metaclass__ = reflection.GeneratedProtocolMessageType 127 | DESCRIPTOR = _DEVICECONFIGURATIONMESSAGE_NAME 128 | 129 | # @@protoc_insertion_point(class_scope:DeviceConfigurationMessage.Name) 130 | 131 | class DeviceConfiguration(message.Message): 132 | __metaclass__ = reflection.GeneratedProtocolMessageType 133 | DESCRIPTOR = _DEVICECONFIGURATIONMESSAGE_DEVICECONFIGURATION 134 | 135 | # @@protoc_insertion_point(class_scope:DeviceConfigurationMessage.DeviceConfiguration) 136 | DESCRIPTOR = _DEVICECONFIGURATIONMESSAGE 137 | 138 | # @@protoc_insertion_point(class_scope:DeviceConfigurationMessage) 139 | 140 | # @@protoc_insertion_point(module_scope) 141 | -------------------------------------------------------------------------------- /ndn_pi/commands/send-pairing-info.proto: -------------------------------------------------------------------------------- 1 | message DevicePairingInfoMessage { 2 | message DevicePairingInfo { 3 | required string deviceSerial = 236; 4 | required bytes devicePin = 237; 5 | required string deviceSuffix = 238; 6 | } 7 | required DevicePairingInfo info = 239; 8 | } 9 | -------------------------------------------------------------------------------- /ndn_pi/commands/send_pairing_info_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | 3 | from google.protobuf import descriptor 4 | from google.protobuf import message 5 | from google.protobuf import reflection 6 | from google.protobuf import descriptor_pb2 7 | # @@protoc_insertion_point(imports) 8 | 9 | 10 | 11 | DESCRIPTOR = descriptor.FileDescriptor( 12 | name='send-pairing-info.proto', 13 | package='', 14 | serialized_pb='\n\x17send-pairing-info.proto\"\xad\x01\n\x18\x44\x65vicePairingInfoMessage\x12:\n\x04info\x18\xee\x01 \x02(\x0b\x32+.DevicePairingInfoMessage.DevicePairingInfo\x1aU\n\x11\x44\x65vicePairingInfo\x12\x15\n\x0c\x64\x65viceSerial\x18\xeb\x01 \x02(\t\x12\x12\n\tdevicePin\x18\xec\x01 \x02(\x0c\x12\x15\n\x0c\x64\x65viceSuffix\x18\xed\x01 \x02(\t') 15 | 16 | 17 | 18 | 19 | _DEVICEPAIRINGINFOMESSAGE_DEVICEPAIRINGINFO = descriptor.Descriptor( 20 | name='DevicePairingInfo', 21 | full_name='DevicePairingInfoMessage.DevicePairingInfo', 22 | filename=None, 23 | file=DESCRIPTOR, 24 | containing_type=None, 25 | fields=[ 26 | descriptor.FieldDescriptor( 27 | name='deviceSerial', full_name='DevicePairingInfoMessage.DevicePairingInfo.deviceSerial', index=0, 28 | number=235, type=9, cpp_type=9, label=2, 29 | has_default_value=False, default_value=unicode("", "utf-8"), 30 | message_type=None, enum_type=None, containing_type=None, 31 | is_extension=False, extension_scope=None, 32 | options=None), 33 | descriptor.FieldDescriptor( 34 | name='devicePin', full_name='DevicePairingInfoMessage.DevicePairingInfo.devicePin', index=1, 35 | number=236, type=12, cpp_type=9, label=2, 36 | has_default_value=False, default_value="", 37 | message_type=None, enum_type=None, containing_type=None, 38 | is_extension=False, extension_scope=None, 39 | options=None), 40 | descriptor.FieldDescriptor( 41 | name='deviceSuffix', full_name='DevicePairingInfoMessage.DevicePairingInfo.deviceSuffix', index=2, 42 | number=237, type=9, cpp_type=9, label=2, 43 | has_default_value=False, default_value=unicode("", "utf-8"), 44 | message_type=None, enum_type=None, containing_type=None, 45 | is_extension=False, extension_scope=None, 46 | options=None), 47 | ], 48 | extensions=[ 49 | ], 50 | nested_types=[], 51 | enum_types=[ 52 | ], 53 | options=None, 54 | is_extendable=False, 55 | extension_ranges=[], 56 | serialized_start=116, 57 | serialized_end=201, 58 | ) 59 | 60 | _DEVICEPAIRINGINFOMESSAGE = descriptor.Descriptor( 61 | name='DevicePairingInfoMessage', 62 | full_name='DevicePairingInfoMessage', 63 | filename=None, 64 | file=DESCRIPTOR, 65 | containing_type=None, 66 | fields=[ 67 | descriptor.FieldDescriptor( 68 | name='info', full_name='DevicePairingInfoMessage.info', index=0, 69 | number=238, type=11, cpp_type=10, label=2, 70 | has_default_value=False, default_value=None, 71 | message_type=None, enum_type=None, containing_type=None, 72 | is_extension=False, extension_scope=None, 73 | options=None), 74 | ], 75 | extensions=[ 76 | ], 77 | nested_types=[_DEVICEPAIRINGINFOMESSAGE_DEVICEPAIRINGINFO, ], 78 | enum_types=[ 79 | ], 80 | options=None, 81 | is_extendable=False, 82 | extension_ranges=[], 83 | serialized_start=28, 84 | serialized_end=201, 85 | ) 86 | 87 | _DEVICEPAIRINGINFOMESSAGE_DEVICEPAIRINGINFO.containing_type = _DEVICEPAIRINGINFOMESSAGE; 88 | _DEVICEPAIRINGINFOMESSAGE.fields_by_name['info'].message_type = _DEVICEPAIRINGINFOMESSAGE_DEVICEPAIRINGINFO 89 | DESCRIPTOR.message_types_by_name['DevicePairingInfoMessage'] = _DEVICEPAIRINGINFOMESSAGE 90 | 91 | class DevicePairingInfoMessage(message.Message): 92 | __metaclass__ = reflection.GeneratedProtocolMessageType 93 | 94 | class DevicePairingInfo(message.Message): 95 | __metaclass__ = reflection.GeneratedProtocolMessageType 96 | DESCRIPTOR = _DEVICEPAIRINGINFOMESSAGE_DEVICEPAIRINGINFO 97 | 98 | # @@protoc_insertion_point(class_scope:DevicePairingInfoMessage.DevicePairingInfo) 99 | DESCRIPTOR = _DEVICEPAIRINGINFOMESSAGE 100 | 101 | # @@protoc_insertion_point(class_scope:DevicePairingInfoMessage) 102 | 103 | # @@protoc_insertion_point(module_scope) 104 | -------------------------------------------------------------------------------- /ndn_pi/commands/update-capabilities.proto: -------------------------------------------------------------------------------- 1 | message UpdateCapabilitiesCommandMessage { 2 | message Name { 3 | repeated bytes components = 8; 4 | } 5 | 6 | message CapabilitiesParameter { 7 | required string parameterType = 228; // should be a protobuf type 8 | optional string parameterDesc = 229; // what is this parameter for? (recommended) 9 | } 10 | 11 | message Capability { 12 | required Name commandPrefix = 230; // the name (after device prefix) of command 13 | repeated string keywords = 231; // other devices can search for one or more keywords 14 | // e.g. 'cec', 'motion', 'thermostat' 15 | repeated CapabilitiesParameter parameterDesc = 232; // list of parameter descriptions 16 | optional bool needsSignature=233; 17 | required String deviceSerial=234; 18 | } 19 | 20 | repeated Capability capabilities = 235; 21 | } 22 | -------------------------------------------------------------------------------- /ndn_pi/commands/update_capabilities_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | 3 | from google.protobuf import descriptor 4 | from google.protobuf import message 5 | from google.protobuf import reflection 6 | from google.protobuf import descriptor_pb2 7 | # @@protoc_insertion_point(imports) 8 | 9 | 10 | 11 | DESCRIPTOR = descriptor.FileDescriptor( 12 | name='update-capabilities.proto', 13 | package='', 14 | serialized_pb='\n\x19update-capabilities.proto\"\x98\x03\n UpdateCapabilitiesCommandMessage\x12\x43\n\x0c\x63\x61pabilities\x18\xea\x01 \x03(\x0b\x32,.UpdateCapabilitiesCommandMessage.Capability\x1a\x1a\n\x04Name\x12\x12\n\ncomponents\x18\x08 \x03(\x0c\x1aG\n\x15\x43\x61pabilitiesParameter\x12\x16\n\rparameterType\x18\xe4\x01 \x02(\t\x12\x16\n\rparameterDesc\x18\xe5\x01 \x01(\t\x1a\xc9\x01\n\nCapability\x12>\n\rcommandPrefix\x18\xe6\x01 \x02(\x0b\x32&.UpdateCapabilitiesCommandMessage.Name\x12\x11\n\x08keywords\x18\xe7\x01 \x03(\t\x12O\n\rparameterDesc\x18\xe8\x01 \x03(\x0b\x32\x37.UpdateCapabilitiesCommandMessage.CapabilitiesParameter\x12\x17\n\x0eneedsSignature\x18\xe9\x01 \x01(\x08') 15 | 16 | 17 | 18 | 19 | _UPDATECAPABILITIESCOMMANDMESSAGE_NAME = descriptor.Descriptor( 20 | name='Name', 21 | full_name='UpdateCapabilitiesCommandMessage.Name', 22 | filename=None, 23 | file=DESCRIPTOR, 24 | containing_type=None, 25 | fields=[ 26 | descriptor.FieldDescriptor( 27 | name='components', full_name='UpdateCapabilitiesCommandMessage.Name.components', index=0, 28 | number=8, type=12, cpp_type=9, label=3, 29 | has_default_value=False, default_value=[], 30 | message_type=None, enum_type=None, containing_type=None, 31 | is_extension=False, extension_scope=None, 32 | options=None), 33 | ], 34 | extensions=[ 35 | ], 36 | nested_types=[], 37 | enum_types=[ 38 | ], 39 | options=None, 40 | is_extendable=False, 41 | extension_ranges=[], 42 | serialized_start=135, 43 | serialized_end=161, 44 | ) 45 | 46 | _UPDATECAPABILITIESCOMMANDMESSAGE_CAPABILITIESPARAMETER = descriptor.Descriptor( 47 | name='CapabilitiesParameter', 48 | full_name='UpdateCapabilitiesCommandMessage.CapabilitiesParameter', 49 | filename=None, 50 | file=DESCRIPTOR, 51 | containing_type=None, 52 | fields=[ 53 | descriptor.FieldDescriptor( 54 | name='parameterType', full_name='UpdateCapabilitiesCommandMessage.CapabilitiesParameter.parameterType', index=0, 55 | number=228, type=9, cpp_type=9, label=2, 56 | has_default_value=False, default_value=unicode("", "utf-8"), 57 | message_type=None, enum_type=None, containing_type=None, 58 | is_extension=False, extension_scope=None, 59 | options=None), 60 | descriptor.FieldDescriptor( 61 | name='parameterDesc', full_name='UpdateCapabilitiesCommandMessage.CapabilitiesParameter.parameterDesc', index=1, 62 | number=229, type=9, cpp_type=9, label=1, 63 | has_default_value=False, default_value=unicode("", "utf-8"), 64 | message_type=None, enum_type=None, containing_type=None, 65 | is_extension=False, extension_scope=None, 66 | options=None), 67 | ], 68 | extensions=[ 69 | ], 70 | nested_types=[], 71 | enum_types=[ 72 | ], 73 | options=None, 74 | is_extendable=False, 75 | extension_ranges=[], 76 | serialized_start=163, 77 | serialized_end=234, 78 | ) 79 | 80 | _UPDATECAPABILITIESCOMMANDMESSAGE_CAPABILITY = descriptor.Descriptor( 81 | name='Capability', 82 | full_name='UpdateCapabilitiesCommandMessage.Capability', 83 | filename=None, 84 | file=DESCRIPTOR, 85 | containing_type=None, 86 | fields=[ 87 | descriptor.FieldDescriptor( 88 | name='commandPrefix', full_name='UpdateCapabilitiesCommandMessage.Capability.commandPrefix', index=0, 89 | number=230, type=11, cpp_type=10, label=2, 90 | has_default_value=False, default_value=None, 91 | message_type=None, enum_type=None, containing_type=None, 92 | is_extension=False, extension_scope=None, 93 | options=None), 94 | descriptor.FieldDescriptor( 95 | name='keywords', full_name='UpdateCapabilitiesCommandMessage.Capability.keywords', index=1, 96 | number=231, type=9, cpp_type=9, label=3, 97 | has_default_value=False, default_value=[], 98 | message_type=None, enum_type=None, containing_type=None, 99 | is_extension=False, extension_scope=None, 100 | options=None), 101 | descriptor.FieldDescriptor( 102 | name='parameterDesc', full_name='UpdateCapabilitiesCommandMessage.Capability.parameterDesc', index=2, 103 | number=232, type=11, cpp_type=10, label=3, 104 | has_default_value=False, default_value=[], 105 | message_type=None, enum_type=None, containing_type=None, 106 | is_extension=False, extension_scope=None, 107 | options=None), 108 | descriptor.FieldDescriptor( 109 | name='needsSignature', full_name='UpdateCapabilitiesCommandMessage.Capability.needsSignature', index=3, 110 | number=233, type=8, cpp_type=7, label=1, 111 | has_default_value=False, default_value=False, 112 | message_type=None, enum_type=None, containing_type=None, 113 | is_extension=False, extension_scope=None, 114 | options=None), 115 | ], 116 | extensions=[ 117 | ], 118 | nested_types=[], 119 | enum_types=[ 120 | ], 121 | options=None, 122 | is_extendable=False, 123 | extension_ranges=[], 124 | serialized_start=237, 125 | serialized_end=438, 126 | ) 127 | 128 | _UPDATECAPABILITIESCOMMANDMESSAGE = descriptor.Descriptor( 129 | name='UpdateCapabilitiesCommandMessage', 130 | full_name='UpdateCapabilitiesCommandMessage', 131 | filename=None, 132 | file=DESCRIPTOR, 133 | containing_type=None, 134 | fields=[ 135 | descriptor.FieldDescriptor( 136 | name='capabilities', full_name='UpdateCapabilitiesCommandMessage.capabilities', index=0, 137 | number=234, type=11, cpp_type=10, label=3, 138 | has_default_value=False, default_value=[], 139 | message_type=None, enum_type=None, containing_type=None, 140 | is_extension=False, extension_scope=None, 141 | options=None), 142 | ], 143 | extensions=[ 144 | ], 145 | nested_types=[_UPDATECAPABILITIESCOMMANDMESSAGE_NAME, _UPDATECAPABILITIESCOMMANDMESSAGE_CAPABILITIESPARAMETER, _UPDATECAPABILITIESCOMMANDMESSAGE_CAPABILITY, ], 146 | enum_types=[ 147 | ], 148 | options=None, 149 | is_extendable=False, 150 | extension_ranges=[], 151 | serialized_start=30, 152 | serialized_end=438, 153 | ) 154 | 155 | _UPDATECAPABILITIESCOMMANDMESSAGE_NAME.containing_type = _UPDATECAPABILITIESCOMMANDMESSAGE; 156 | _UPDATECAPABILITIESCOMMANDMESSAGE_CAPABILITIESPARAMETER.containing_type = _UPDATECAPABILITIESCOMMANDMESSAGE; 157 | _UPDATECAPABILITIESCOMMANDMESSAGE_CAPABILITY.fields_by_name['commandPrefix'].message_type = _UPDATECAPABILITIESCOMMANDMESSAGE_NAME 158 | _UPDATECAPABILITIESCOMMANDMESSAGE_CAPABILITY.fields_by_name['parameterDesc'].message_type = _UPDATECAPABILITIESCOMMANDMESSAGE_CAPABILITIESPARAMETER 159 | _UPDATECAPABILITIESCOMMANDMESSAGE_CAPABILITY.containing_type = _UPDATECAPABILITIESCOMMANDMESSAGE; 160 | _UPDATECAPABILITIESCOMMANDMESSAGE.fields_by_name['capabilities'].message_type = _UPDATECAPABILITIESCOMMANDMESSAGE_CAPABILITY 161 | DESCRIPTOR.message_types_by_name['UpdateCapabilitiesCommandMessage'] = _UPDATECAPABILITIESCOMMANDMESSAGE 162 | 163 | class UpdateCapabilitiesCommandMessage(message.Message): 164 | __metaclass__ = reflection.GeneratedProtocolMessageType 165 | 166 | class Name(message.Message): 167 | __metaclass__ = reflection.GeneratedProtocolMessageType 168 | DESCRIPTOR = _UPDATECAPABILITIESCOMMANDMESSAGE_NAME 169 | 170 | # @@protoc_insertion_point(class_scope:UpdateCapabilitiesCommandMessage.Name) 171 | 172 | class CapabilitiesParameter(message.Message): 173 | __metaclass__ = reflection.GeneratedProtocolMessageType 174 | DESCRIPTOR = _UPDATECAPABILITIESCOMMANDMESSAGE_CAPABILITIESPARAMETER 175 | 176 | # @@protoc_insertion_point(class_scope:UpdateCapabilitiesCommandMessage.CapabilitiesParameter) 177 | 178 | class Capability(message.Message): 179 | __metaclass__ = reflection.GeneratedProtocolMessageType 180 | DESCRIPTOR = _UPDATECAPABILITIESCOMMANDMESSAGE_CAPABILITY 181 | 182 | # @@protoc_insertion_point(class_scope:UpdateCapabilitiesCommandMessage.Capability) 183 | DESCRIPTOR = _UPDATECAPABILITIESCOMMANDMESSAGE 184 | 185 | # @@protoc_insertion_point(class_scope:UpdateCapabilitiesCommandMessage) 186 | 187 | # @@protoc_insertion_point(module_scope) 188 | -------------------------------------------------------------------------------- /ndn_pi/dialog.py: -------------------------------------------------------------------------------- 1 | # -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */ 2 | # 3 | # Copyright (C) 2014 Regents of the University of California. 4 | # Author: Adeola Bannis 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Lesser General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # A copy of the GNU General Public License is in the file COPYING. 19 | 20 | # Modified from whiptail.py, copyright (C) 2013 Marwan Alsabbagh 21 | # https://github.com/marwano/whiptail 22 | 23 | import sys 24 | import shlex 25 | import itertools 26 | from subprocess import Popen, PIPE 27 | from collections import namedtuple 28 | import os 29 | 30 | __version__ = '0.1.dev' 31 | PY3 = sys.version_info[0] == 3 32 | string_types = str if PY3 else basestring 33 | Response = namedtuple('Response', 'returncode value') 34 | 35 | #TODO: rename preExtras/postExtras/extra to more sensible names 36 | 37 | def flatten(data): 38 | return list(itertools.chain.from_iterable(data)) 39 | 40 | class Dialog(object): 41 | DIALOG_OK = DIALOG_YES = 0 42 | DIALOG_CANCEL = DIALOG_NO = 1 43 | DIALOG_HELP = 2 44 | DIALOG_EXTRA = 3 45 | DIALOG_ESC = 255 46 | 47 | def __init__(self, title='', backtitle='', height=10, width=50): 48 | self.title = title 49 | self.backtitle = backtitle 50 | self.height = height 51 | self.width = width 52 | 53 | def helpFileName(self, fileName): 54 | return os.path.join(self.pathName, 'help', fileName) 55 | 56 | 57 | def run(self, control, msg, preExtra= (), postExtra=(), exit_on=(1, 255)): 58 | cmd = ['dialog'] 59 | 60 | if len(self.title) > 0: 61 | cmd.extend(['--title', self.title]) 62 | 63 | if len(self.backtitle) > 0: 64 | cmd.extend(['--backtitle', self.backtitle]) 65 | 66 | 67 | cmd.extend(list(preExtra)) 68 | 69 | cmd.extend(['--'+control, msg , str(self.height), str(self.width)]) 70 | cmd.extend(list(postExtra)) 71 | 72 | p = Popen(cmd, stderr=PIPE) 73 | out, err = p.communicate() 74 | toReturn = Response(p.returncode, err) 75 | return toReturn 76 | 77 | def prompt(self, msg, default='', preExtra=(), password=False): 78 | control = 'passwordbox' if password else 'inputbox' 79 | return self.run(control, msg, postExtra=[default], preExtra=preExtra) 80 | 81 | def confirm(self, msg, default='yes'): 82 | defaultno = '--defaultno' if default == 'no' else '' 83 | return self.run('yesno', msg, postExtra=[defaultno], exit_on=[255]).returncode == 0 84 | 85 | def alert(self, msg, showButtons=True, preExtra=()): 86 | if not showButtons: 87 | self.run('infobox', msg, preExtra=preExtra) 88 | else: 89 | self.run('msgbox', msg, preExtra=preExtra) 90 | 91 | def view_file(self, path): 92 | self.run('textbox', path, postExtra=['--scrolltext']) 93 | 94 | def calc_height(self, msg): 95 | height_offset = 8 if msg else 7 96 | return [str(self.height - height_offset)] 97 | 98 | def mainMenu(self, msg='', items=(), preExtras=(), prefix = ' ', postExtras=(), 99 | okLabel='Select'): 100 | allPreExtras = list(preExtras) 101 | allPreExtras.extend(['--no-cancel', '--ok-label', okLabel]) 102 | 103 | return self.menu(msg=msg, items=items, preExtras=allPreExtras, prefix=prefix, extras=postExtras) 104 | 105 | def insertDeleteMenu(self, msg='', items=(), preExtras=(), prefix = '', postExtras=(), 106 | cancelLabel='Back', editLabel='Edit', insertLabel='Add', deleteLabel=None): 107 | # the ordering of buttons leaves something to be desired... 108 | allPreExtras = list(preExtras) 109 | 110 | if editLabel is None: 111 | allPreExtras.extend(['--no-ok']) 112 | else: 113 | allPreExtras.extend(['--ok-label', editLabel]) 114 | 115 | if insertLabel is not None: 116 | allPreExtras.extend(['--extra-button', '--extra-label', insertLabel]) 117 | 118 | if deleteLabel is not None: 119 | allPreExtras.extend(['--help-button', '--help-label', deleteLabel]) 120 | 121 | allPreExtras.extend(['--cancel-label', cancelLabel]) 122 | 123 | response = self.menu(msg=msg, items=items, 124 | preExtras=allPreExtras, prefix=prefix, extras=postExtras) 125 | # remove the 'HELP' string from the help button respons 126 | if response.returncode == self.DIALOG_HELP: 127 | response = Response(response.returncode, response.value.strip('HELP ')) 128 | return response 129 | 130 | 131 | def menu(self, msg='', items=(), preExtras = (), prefix = '', extras=()): 132 | if len(items) > 0: 133 | if isinstance(items[0], basestring): 134 | items = [(i, '') for i in items] 135 | else: 136 | items = [(k, prefix + v) for k,v in items] 137 | extra = self.calc_height(msg) + flatten(items) 138 | extra.extend(extras) 139 | return self.run('menu', msg, preExtras, extra) 140 | 141 | def showlist(self, control, msg, items, prefix): 142 | if isinstance(items[0], string_types): 143 | items = [(i, '', 'OFF') for i in items] 144 | else: 145 | items = [(k, prefix + v, s) for k, v, s in items] 146 | extra = self.calc_height(msg) + flatten(items) 147 | returnTuple = self.run(control, msg, postExtra=extra) 148 | return Response(returnTuple.returnCode, shlex.split(returnTuple.value)) 149 | 150 | def radiolist(self, msg='', items=(), prefix=' - '): 151 | return self.showlist('radiolist', msg, items, prefix) 152 | 153 | def checklist(self, msg='', items=(), prefix=' - '): 154 | return self.showlist('checklist', msg, items, prefix) 155 | 156 | def fileSelection(self, msg='', startDirectory='./', preExtras=(), 157 | postExtras=(), directoriesOnly=False): 158 | 159 | # some instructions are in order 160 | preExtras = list(preExtras) 161 | preExtras.extend(['--hline', msg]) 162 | 163 | if directoriesOnly: 164 | commandName = 'dselect' 165 | else: 166 | commandName = 'fselect' 167 | returnCode, value = self.run(commandName, startDirectory, preExtras, postExtras) 168 | # handle incomplete directory names here 169 | # this is not the most user-friendly dialog 170 | while returnCode == self.DIALOG_OK: 171 | #sanitize the value 172 | if os.path.isdir(value): 173 | value = os.path.normpath(value) 174 | break 175 | elif len(value) > 0: 176 | if os.path.isdir(os.path.dirname(value)): 177 | startDirectory = value 178 | returnCode, value = self.run(commandName, startDirectory, preExtras, postExtras) 179 | return Response(returnCode, value) 180 | 181 | def form(self, msg='', formFieldInfo=[], preExtras=(), postExtras=(), 182 | margin=2, extraLabel= None): 183 | # compose form field information 184 | # first find the longest field, so we can align the input fields 185 | longestField = max(formFieldInfo, key=lambda x:len(x.label)) 186 | inputStart = margin + len(longestField.label) + 2 187 | 188 | fields = [] 189 | # now make the list of arguments 190 | y = 1 # don't jam the form into the top of the menu... 191 | for field in formFieldInfo: 192 | flen = field.maxLength if field.isEditable else 0 193 | ftype = 1 if field.isPassword else 0 194 | fields.extend([field.label, str(y), str(margin), 195 | field.default, str(y), str(inputStart), 196 | str(flen), str(field.maxLength), str(ftype)]) 197 | y += 2 198 | 199 | postExtras = list(postExtras) 200 | preExtras = list(preExtras) 201 | if extraLabel is not None: 202 | preExtras.extend(['--extra-button', '--extra-label', extraLabel]) 203 | 204 | preExtras.extend(['--insecure']) 205 | 206 | extra = self.calc_height(msg) + fields + postExtras 207 | response = self.run('mixedform', msg, preExtras, extra) 208 | # similar to showlist, let's split up the return info for convenience 209 | # need to remove the trailing newline though 210 | response = Response(response.returncode, 211 | [v.strip() for v in response.value[:-1].split('\n')]) 212 | return response 213 | 214 | def gauge(self, msg, percent): 215 | extra = [str(percent)] 216 | return self.run('mixedgauge', msg, [], extra) 217 | 218 | class FormField(object): 219 | """ 220 | Encapsulate the name, default value and maxLength of a field 221 | """ 222 | def __init__(self, label='', default='', maxLength=100, 223 | isPassword=False, isEditable=True): 224 | super(Dialog.FormField, self).__init__() 225 | self.label = label 226 | self.default = default 227 | self.maxLength = maxLength 228 | self.isPassword = isPassword 229 | self.isEditable = isEditable 230 | 231 | -------------------------------------------------------------------------------- /ndn_pi/iot_console.py: -------------------------------------------------------------------------------- 1 | # -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */ 2 | # 3 | # Copyright (C) 2014 Regents of the University of California. 4 | # Author: Adeola Bannis 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Lesser General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # A copy of the GNU General Public License is in the file COPYING. 19 | 20 | from __future__ import print_function 21 | 22 | import logging 23 | 24 | from pyndn import Name, Face, Interest, Data, ThreadsafeFace 25 | from pyndn.util import Blob 26 | from pyndn.security import KeyChain 27 | from pyndn.encoding import ProtobufTlv 28 | from pyndn.security.security_exception import SecurityException 29 | 30 | from ndn_pi.security import IotIdentityStorage,IotPolicyManager, IotIdentityManager 31 | from commands import DevicePairingInfoMessage 32 | 33 | from collections import defaultdict, OrderedDict 34 | import json 35 | import base64 36 | 37 | from dialog import Dialog 38 | import logging 39 | try: 40 | import asyncio 41 | except ImportError: 42 | import trollius as asyncio 43 | 44 | # more Python 2+3 compatibility 45 | try: 46 | input = raw_input 47 | except NameError: 48 | pass 49 | 50 | class IotConsole(object): 51 | """ 52 | This uses the controller's credentials to provide a management interface 53 | to the user. 54 | It does not go through the security handshake (as it should be run on the 55 | same device as the controller) and so does not inherit from the BaseNode. 56 | """ 57 | def __init__(self, networkName, nodeName): 58 | super(IotConsole, self).__init__() 59 | 60 | self.deviceSuffix = Name(nodeName) 61 | self.networkPrefix = Name(networkName) 62 | self.prefix = Name(self.networkPrefix).append(self.deviceSuffix) 63 | 64 | self._identityStorage = IotIdentityStorage() 65 | self._policyManager = IotPolicyManager(self._identityStorage) 66 | self._identityManager = IotIdentityManager(self._identityStorage) 67 | self._keyChain = KeyChain(self._identityManager, self._policyManager) 68 | 69 | self._policyManager.setEnvironmentPrefix(self.networkPrefix) 70 | self._policyManager.setTrustRootIdentity(self.prefix) 71 | self._policyManager.setDeviceIdentity(self.prefix) 72 | self._policyManager.updateTrustRules() 73 | 74 | self.foundCommands = {} 75 | self.unconfiguredDevices = [] 76 | 77 | # TODO: use xDialog in XWindows 78 | self.ui = Dialog(backtitle='NDN IoT User Console', height=18, width=78) 79 | 80 | trolliusLogger = logging.getLogger('trollius') 81 | trolliusLogger.addHandler(logging.StreamHandler()) 82 | 83 | def start(self): 84 | """ 85 | Start up the UI 86 | """ 87 | self.loop = asyncio.get_event_loop() 88 | self.face = ThreadsafeFace(self.loop, '') 89 | 90 | controllerCertificateName = self._identityStorage.getDefaultCertificateNameForIdentity(self.prefix) 91 | self.face.setCommandSigningInfo(self._keyChain, controllerCertificateName) 92 | self._keyChain.setFace(self.face) # shouldn't be necessarym but doesn't hurt 93 | 94 | self._isStopped = False 95 | self.face.stopWhen(lambda:self._isStopped) 96 | 97 | self.loop.call_soon(self.displayMenu) 98 | try: 99 | self.loop.run_forever() 100 | except KeyboardInterrupt: 101 | pass 102 | except Exception as e: 103 | print(e) 104 | #self.log('Exception', e) 105 | finally: 106 | self._isStopped = True 107 | 108 | def stop(self): 109 | self._isStopped = True 110 | 111 | ####### 112 | # GUI 113 | ####### 114 | 115 | def displayMenu(self): 116 | 117 | menuOptions = OrderedDict([('List network services',self.listCommands), 118 | ('Pair a device',self.pairDevice), 119 | ('Express interest',self.expressInterest), 120 | ('Quit',self.stop) 121 | ]) 122 | (retCode, retStr) = self.ui.mainMenu('Main Menu', 123 | menuOptions.keys()) 124 | 125 | 126 | if retCode == Dialog.DIALOG_ESC or retCode == Dialog.DIALOG_CANCEL: 127 | # TODO: ask if you're sure you want to quit 128 | self.stop() 129 | if retCode == Dialog.DIALOG_OK: 130 | menuOptions[retStr]() 131 | 132 | 133 | ####### 134 | # List all commands 135 | ###### 136 | def listCommands(self): 137 | self._requestDeviceList(self._showCommandList, self.displayMenu) 138 | 139 | def _requestDeviceList(self, successCallback, timeoutCallback): 140 | self.ui.alert('Requesting services list...', False) 141 | interestName = Name(self.prefix).append('listCommands') 142 | interest = Interest(interestName) 143 | interest.setInterestLifetimeMilliseconds(3000) 144 | #self.face.makeCommandInterest(interest) 145 | self.face.expressInterest(interest, 146 | self._makeOnCommandListCallback(successCallback), 147 | self._makeOnCommandListTimeoutCallback(timeoutCallback)) 148 | 149 | def _makeOnCommandListTimeoutCallback(self, callback): 150 | def onCommandListTimeout(interest): 151 | self.ui.alert('Timed out waiting for services list') 152 | self.loop.call_soon(callback) 153 | return onCommandListTimeout 154 | 155 | def _makeOnCommandListCallback(self, callback): 156 | def onCommandListReceived(interest, data): 157 | try: 158 | commandInfo = json.loads(str(data.getContent())) 159 | except: 160 | self.ui.alert('An error occured while reading the services list') 161 | self.loop.call_soon(self.displayMenu) 162 | else: 163 | self.foundCommands = commandInfo 164 | self.loop.call_soon(callback) 165 | return onCommandListReceived 166 | 167 | def _showCommandList(self): 168 | try: 169 | commandList = [] 170 | for capability, commands in self.foundCommands.items(): 171 | commandList.append('\Z1{}:'.format(capability)) 172 | for info in commands: 173 | signingStr = 'signed' if info['signed'] else 'unsigned' 174 | commandList.append('\Z0\t{} ({})'.format(info['name'], signingStr)) 175 | commandList.append('') 176 | 177 | if len(commandList) == 0: 178 | # should not happen 179 | commandList = ['----NONE----'] 180 | allCommands = '\n'.join(commandList) 181 | oldTitle = self.ui.title 182 | self.ui.title = 'Available services' 183 | self.ui.alert(allCommands, preExtra=['--colors']) 184 | self.ui.title = oldTitle 185 | #self.ui.menu('Available services', commandList, prefix='', extras=['--no-cancel']) 186 | finally: 187 | self.loop.call_soon(self.displayMenu) 188 | 189 | ####### 190 | # New device 191 | ###### 192 | 193 | def pairDevice(self): 194 | self._scanForUnconfiguredDevices() 195 | 196 | 197 | def _enterPairingInfo(self, serial, pin='', newName=''): 198 | fields = [Dialog.FormField('PIN', pin), 199 | Dialog.FormField('Device name', newName)] 200 | (retCode, retList) = self.ui.form('Pairing device {}'.format(serial) 201 | , fields) 202 | if retCode == Dialog.DIALOG_OK: 203 | pin, newName = retList 204 | if len(pin)==0 or len(newName)==0: 205 | self.ui.alert('All fields are required') 206 | self.loop.call_soon(self._enterPairingInfo, serial, pin, newName) 207 | else: 208 | try: 209 | pinBytes = pin.decode('hex') 210 | except TypeError: 211 | self.ui.alert('Pin is invalid') 212 | self.loop.call_soon(self._enterPairingInfo, serial, pin, newName) 213 | else: 214 | self._addDeviceToNetwork(serial, newName, 215 | pin.decode('hex')) 216 | elif retCode == Dialog.DIALOG_CANCEL or retCode == Dialog.DIALOG_ESC: 217 | self.loop.call_soon(self._showConfigurationList) 218 | 219 | def _showConfigurationList(self): 220 | foundDevices = self.unconfiguredDevices[:] 221 | emptyStr = '----NONE----' 222 | if len(foundDevices) == 0: 223 | foundDevices.append(emptyStr) 224 | retCode, retStr = self.ui.menu('Select a device to configure', 225 | foundDevices, preExtras = ['--ok-label', 'Configure', 226 | '--extra-button', 227 | '--extra-label', 'Refresh']) 228 | if retCode == Dialog.DIALOG_CANCEL or retCode == Dialog.DIALOG_ESC: 229 | self.loop.call_soon(self.displayMenu) 230 | elif retCode == Dialog.DIALOG_EXTRA: 231 | self.loop.call_soon(self._scanForUnconfiguredDevices) 232 | elif retCode == Dialog.DIALOG_OK and retStr != emptyStr: 233 | self.loop.call_soon(self._enterPairingInfo, retStr) 234 | else: 235 | self.loop.call_soon(self._showConfigurationList) 236 | 237 | def _scanForUnconfiguredDevices(self): 238 | # unconfigured devices should register '/localhop/configure' 239 | # we keep asking for unconfigured devices until we stop getting replies 240 | 241 | foundDevices = [] 242 | 243 | self.ui.alert('Scanning for unconfigured devices...', False) 244 | 245 | def onDeviceTimeout(interest): 246 | # assume we're done - everyone is excluded 247 | self.unconfiguredDevices = foundDevices 248 | self.loop.call_soon(self._showConfigurationList) 249 | 250 | def onDeviceResponse(interest, data): 251 | updatedInterest = Interest(interest) 252 | deviceSerial = str(data.getContent()) 253 | if len(deviceSerial) > 0: 254 | foundDevices.append(deviceSerial) 255 | updatedInterest.getExclude().appendComponent(Name.Component(deviceSerial)) 256 | # else ignore the malformed response 257 | self.face.expressInterest(updatedInterest, onDeviceResponse, onDeviceTimeout) 258 | 259 | interest= Interest(Name('/localhop/configure')) 260 | interest.setInterestLifetimeMilliseconds(2000) 261 | self.face.expressInterest(interest, onDeviceResponse, onDeviceTimeout) 262 | 263 | 264 | def _addDeviceToNetwork(self, serial, suffix, pin): 265 | self.ui.alert('Sending pairing info to gateway...', False) 266 | # we must encrypt this so no one can see the pin! 267 | message = DevicePairingInfoMessage() 268 | message.info.deviceSuffix = suffix 269 | message.info.deviceSerial = serial 270 | message.info.devicePin = pin 271 | rawBytes = ProtobufTlv.encode(message) 272 | encryptedBytes = self._identityManager.encryptForIdentity(rawBytes, self.prefix) 273 | encodedBytes = base64.urlsafe_b64encode(str(encryptedBytes)) 274 | interestName = Name(self.prefix).append('addDevice').append(encodedBytes) 275 | interest = Interest(interestName) 276 | # todo: have the controller register this console as a listener 277 | # and update it with pairing status 278 | interest.setInterestLifetimeMilliseconds(5000) 279 | self.face.makeCommandInterest(interest) 280 | 281 | self.face.expressInterest(interest, self._onAddDeviceResponse, 282 | self._onAddDeviceTimeout) 283 | 284 | def _onAddDeviceResponse(self, interest, data): 285 | try: 286 | responseCode = int(str(data.getContent())) 287 | if responseCode == 202: 288 | self.ui.alert('Gateway received pairing info') 289 | else: 290 | self.ui.alert('Error encountered while sending pairing info') 291 | except: 292 | self.ui.alert('Exception encountered while decoding gateway reply') 293 | finally: 294 | self.loop.call_soon(self.displayMenu) 295 | 296 | def _onAddDeviceTimeout(self, interest): 297 | self.ui.alert('Timed out sending pairing info to gateway') 298 | self.loop.call_soon(self.displayMenu) 299 | 300 | 301 | ###### 302 | # Express interest 303 | ##### 304 | 305 | 306 | def expressInterest(self): 307 | if len(self.foundCommands) == 0: 308 | self._requestDeviceList(self._showInterestMenu, self._showInterestMenu) 309 | else: 310 | self.loop.call_soon(self._showInterestMenu) 311 | 312 | def _showInterestMenu(self): 313 | # display a menu of all available interests, on selection allow user to 314 | # (1) extend it 315 | # (2) send it signed/unsigned 316 | # NOTE: should it add custom ones to the list? 317 | commandSet = set() 318 | wildcard = '' 319 | try: 320 | for commands in self.foundCommands.values(): 321 | commandSet.update([c['name'] for c in commands]) 322 | commandList = list(commandSet) 323 | commandList.append(wildcard) 324 | (returnCode, returnStr) = self.ui.menu('Choose a command', 325 | commandList, prefix=' ') 326 | if returnCode == Dialog.DIALOG_OK: 327 | if returnStr == wildcard: 328 | returnStr = self.networkPrefix.toUri() 329 | self.loop.call_soon(self._expressCustomInterest, returnStr) 330 | else: 331 | self.loop.call_soon(self.displayMenu) 332 | except: 333 | self.loop.call_soon(self.displayMenu) 334 | 335 | def _expressCustomInterest(self, interestName): 336 | #TODO: make this a form, add timeout field 337 | try: 338 | handled = False 339 | (returnCode, returnStr) = self.ui.prompt('Send interest', 340 | interestName, 341 | preExtra = ['--extra-button', 342 | '--extra-label', 'Signed', 343 | '--ok-label', 'Unsigned']) 344 | if returnCode==Dialog.DIALOG_ESC or returnCode==Dialog.DIALOG_CANCEL: 345 | self.loop.call_soon(self.expressInterest) 346 | else: 347 | interestName = Name(returnStr) 348 | doSigned = (returnCode == Dialog.DIALOG_EXTRA) 349 | interest = Interest(interestName) 350 | interest.setInterestLifetimeMilliseconds(5000) 351 | interest.setChildSelector(1) 352 | interest.setMustBeFresh(True) 353 | if (doSigned): 354 | self.face.makeCommandInterest(interest) 355 | self.ui.alert('Waiting for response to {}'.format(interestName.toUri()), False) 356 | self.face.expressInterest(interest, self.onDataReceived, self.onInterestTimeout) 357 | except: 358 | self.loop.call_soon(self.expressInterest) 359 | 360 | def onInterestTimeout(self, interest): 361 | try: 362 | self.ui.alert('Interest timed out:\n{}'.format(interest.getName().toUri())) 363 | except: 364 | self.ui.alert('Interest timed out') 365 | finally: 366 | self.loop.call_soon(self.expressInterest) 367 | 368 | def onDataReceived(self, interest, data): 369 | try: 370 | dataString = '{}\n\n'.format(data.getName().toUri()) 371 | dataString +='Contents:\n{}'.format(repr(data.getContent().toRawStr())) 372 | self.ui.alert(dataString) 373 | except: 374 | self.ui.alert('Exception occured displaying data contents') 375 | finally: 376 | self.loop.call_soon(self.expressInterest) 377 | 378 | if __name__ == '__main__': 379 | import sys 380 | try: 381 | with open('/tmp/ndn-iot/iot.conf') as nameFile: 382 | info = nameFile.readline() 383 | networkName, controllerName = info.strip().split() 384 | except IOError as e: 385 | if e.errno == 2: 386 | print ('Cannot connect to gateway. Please ensure that it is running.') 387 | else: 388 | print ('Unknown error {} encountered. Exiting...'.format(e.errno)) 389 | sys.exit(1) 390 | 391 | n = IotConsole(networkName, controllerName) 392 | n.start() 393 | -------------------------------------------------------------------------------- /ndn_pi/iot_controller.py: -------------------------------------------------------------------------------- 1 | # -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */ 2 | # 3 | # Copyright (C) 2014 Regents of the University of California. 4 | # Author: Adeola Bannis 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Lesser General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # A copy of the GNU General Public License is in the file COPYING. 19 | 20 | 21 | from __future__ import print_function 22 | 23 | import logging 24 | import time 25 | 26 | from pyndn import Name, Face, Interest, Data, ThreadsafeFace 27 | from pyndn.util import Blob 28 | from pyndn.security import KeyChain 29 | from pyndn.security.certificate import IdentityCertificate, PublicKey, CertificateSubjectDescription 30 | from pyndn.encoding import ProtobufTlv 31 | from pyndn.security.security_exception import SecurityException 32 | 33 | from base_node import BaseNode 34 | 35 | from commands import CertificateRequestMessage, UpdateCapabilitiesCommandMessage, DeviceConfigurationMessage, DevicePairingInfoMessage 36 | from security import HmacHelper 37 | 38 | from collections import defaultdict, OrderedDict 39 | import json 40 | import base64 41 | 42 | from dialog import Dialog 43 | import logging 44 | try: 45 | import asyncio 46 | except ImportError: 47 | import trollius as asyncio 48 | 49 | # more Python 2+3 compatibility 50 | try: 51 | input = raw_input 52 | except NameError: 53 | pass 54 | 55 | class IotController(BaseNode): 56 | """ 57 | The controller class has a few built-in commands: 58 | - listCommands: return the names and capabilities of all attached 59 | devices 60 | - certificateRequest: takes public key information and returns name of 61 | new certificate 62 | - updateCapabilities: should be sent periodically from IotNodes to 63 | update their command lists 64 | - addDevice: called by the console to begin pairing, the payload is 65 | encrypted for the controller as it contains a PIN 66 | It is unlikely that you will need to subclass this. 67 | """ 68 | def __init__(self, nodeName, networkName): 69 | super(IotController, self).__init__() 70 | 71 | self.deviceSuffix = Name(nodeName) 72 | self.networkPrefix = Name(networkName) 73 | self.prefix = Name(self.networkPrefix).append(self.deviceSuffix) 74 | 75 | self._policyManager.setEnvironmentPrefix(self.networkPrefix) 76 | self._policyManager.setTrustRootIdentity(self.prefix) 77 | self._policyManager.setDeviceIdentity(self.prefix) 78 | self._policyManager.updateTrustRules() 79 | 80 | # the controller keeps a directory of capabilities->names 81 | self._directory = defaultdict(list) 82 | 83 | # keep track of who's still using HMACs 84 | # key is device serial, value is the HmacHelper 85 | self._hmacDevices = {} 86 | 87 | # our capabilities 88 | self._baseDirectory = {} 89 | 90 | # add the built-ins 91 | self._insertIntoCapabilities('listCommands', 'directory', False) 92 | 93 | # TODO: use xDialog in XWindows 94 | self.ui = Dialog(backtitle='NDN IoT User Console', height=18, width=78) 95 | 96 | self._directory.update(self._baseDirectory) 97 | self.setLogLevel(logging.INFO) 98 | 99 | def _insertIntoCapabilities(self, commandName, keyword, isSigned): 100 | newUri = Name(self.prefix).append(Name(commandName)).toUri() 101 | self._baseDirectory[keyword] = [{'signed':isSigned, 'name':newUri}] 102 | 103 | def beforeLoopStart(self): 104 | if not self._policyManager.hasRootSignedCertificate(): 105 | # make one.... 106 | self.log.warn('Generating controller key pair (this could take a while)...') 107 | newKey = self._identityManager.generateRSAKeyPairAsDefault( 108 | self.prefix, isKsk=True, progressFunc=self._showRSAProgress) 109 | newCert = self._identityManager.selfSign(newKey) 110 | self._identityManager.addCertificateAsDefault(newCert) 111 | self.face.setCommandSigningInfo(self._keyChain, self.getDefaultCertificateName()) 112 | self.face.registerPrefix(self.prefix, 113 | self._onCommandReceived, self.onRegisterFailed) 114 | self.loop.call_soon(self.onStartup) 115 | 116 | 117 | ###### 118 | # Initial device configuration 119 | ####### 120 | 121 | def _beginPairing(self, encryptedMessage): 122 | # base64 decode, decrypt, protobuf decode 123 | responseCode = 202 124 | try: 125 | encryptedBytes = base64.urlsafe_b64decode(str(encryptedMessage.getValue())) 126 | decryptedBytes = self._identityManager.decryptAsIdentity(encryptedBytes, self.prefix) 127 | message = DevicePairingInfoMessage() 128 | ProtobufTlv.decode(message, decryptedBytes) 129 | except: 130 | responseCode = 500 131 | else: 132 | info = message.info 133 | self.loop.call_soon(self._addDeviceToNetwork, info.deviceSerial, 134 | info.deviceSuffix, info.devicePin) 135 | return responseCode 136 | 137 | def _addDeviceToNetwork(self, deviceSerial, newDeviceSuffix, pin): 138 | h = HmacHelper(pin) 139 | self._hmacDevices[deviceSerial] = h 140 | 141 | d = DeviceConfigurationMessage() 142 | 143 | newDeviceSuffix = Name(newDeviceSuffix) 144 | 145 | for source, dest in [(self.networkPrefix, d.configuration.networkPrefix), 146 | (self.deviceSuffix, d.configuration.controllerName), 147 | (newDeviceSuffix, d.configuration.deviceSuffix)]: 148 | for i in range(len(source)): 149 | component = source.get(i) 150 | dest.components.append(component.getValue().toRawStr()) 151 | 152 | interestName = Name('/localhop/configure').append(Name(deviceSerial)) 153 | encodedParams = ProtobufTlv.encode(d) 154 | interestName.append(encodedParams) 155 | interest = Interest(interestName) 156 | interest.setInterestLifetimeMilliseconds(5000) 157 | h.signInterest(interest) 158 | 159 | self.face.expressInterest(interest, self._deviceAdditionResponse, 160 | self._deviceAdditionTimedOut) 161 | 162 | def _deviceAdditionTimedOut(self, interest): 163 | deviceSerial = str(interest.getName().get(2).getValue()) 164 | self.log.warn("Timed out trying to configure device " + deviceSerial) 165 | # don't try again 166 | self._hmacDevices.pop(deviceSerial) 167 | 168 | def _deviceAdditionResponse(self, interest, data): 169 | status = data.getContent().toRawStr() 170 | deviceSerial = str(interest.getName().get(2).getValue()) 171 | hmacChecker = self._hmacDevices[deviceSerial] 172 | if (hmacChecker.verifyData(data)): 173 | self.log.info("Received {} from {}".format(status, deviceSerial)) 174 | else: 175 | self.log.warn("Received invalid HMAC from {}".format(deviceSerial)) 176 | 177 | ###### 178 | # Certificate signing 179 | ###### 180 | 181 | def _handleCertificateRequest(self, interest, transport): 182 | """ 183 | Extracts a public key name and key bits from a command interest name 184 | component. Generates a certificate if the request is verifiable. 185 | 186 | This expects an HMAC signed interest. 187 | """ 188 | message = CertificateRequestMessage() 189 | commandParamsTlv = interest.getName().get(self.prefix.size()+1) 190 | ProtobufTlv.decode(message, commandParamsTlv.getValue()) 191 | 192 | signature = HmacHelper.extractInterestSignature(interest) 193 | deviceSerial = str(signature.getKeyLocator().getKeyName().get(-1).getValue()) 194 | 195 | response = Data(interest.getName()) 196 | certData = None 197 | hmac = None 198 | try: 199 | hmac = self._hmacDevices[deviceSerial] 200 | if hmac.verifyInterest(interest): 201 | certData = self._createCertificateFromRequest(message) 202 | # remove this hmac; another request will require a new pin 203 | self._hmacDevices.pop(deviceSerial) 204 | except KeyError: 205 | self.log.warn('Received certificate request for device with no registered key') 206 | except SecurityException: 207 | self.log.warn('Could not create device certificate') 208 | else: 209 | self.log.info('Creating certificate for device {}'.format(deviceSerial)) 210 | 211 | if certData is not None: 212 | response.setContent(certData.wireEncode()) 213 | response.getMetaInfo().setFreshnessPeriod(10000) # should be good even longer 214 | else: 215 | response.setContent("Denied") 216 | if hmac is not None: 217 | hmac.signData(response) 218 | self.sendData(response, transport, False) 219 | 220 | def _createCertificateFromRequest(self, message): 221 | """ 222 | Generate an IdentityCertificate from the public key information given. 223 | """ 224 | # TODO: Verify the certificate was actually signed with the private key 225 | # matching the public key we are issuing a cert for!! 226 | 227 | keyComponents = message.command.keyName.components 228 | keyName = Name("/".join(keyComponents)) 229 | 230 | self.log.debug("Key name: " + keyName.toUri()) 231 | 232 | if not self._policyManager.getEnvironmentPrefix().match(keyName): 233 | # we do not issue certs for keys outside of our network 234 | return None 235 | 236 | keyDer = Blob(message.command.keyBits) 237 | keyType = message.command.keyType 238 | 239 | try: 240 | self._identityStorage.addKey(keyName, keyType, keyDer) 241 | except SecurityException: 242 | # assume this is due to already existing? 243 | pass 244 | 245 | certificate = self._identityManager.generateCertificateForKey(keyName) 246 | 247 | self._keyChain.sign(certificate, self.getDefaultCertificateName()) 248 | # store it for later use + verification 249 | self._identityStorage.addCertificate(certificate) 250 | return certificate 251 | 252 | ###### 253 | # Device Capabilities 254 | ###### 255 | 256 | def _updateDeviceCapabilities(self, interest): 257 | """ 258 | Take the received capabilities update interest and update our directory listings. 259 | """ 260 | # we assume the sender is the one who signed the interest... 261 | signature = self._policyManager._extractSignature(interest) 262 | certificateName = signature.getKeyLocator().getKeyName() 263 | senderIdentity = IdentityCertificate.certificateNameToPublicKeyName(certificateName).getPrefix(-1) 264 | 265 | self.log.info('Updating capabilities for {}'.format(senderIdentity.toUri())) 266 | 267 | # get the params from the interest name 268 | messageComponent = interest.getName().get(self.prefix.size()+1) 269 | message = UpdateCapabilitiesCommandMessage() 270 | ProtobufTlv.decode(message, messageComponent.getValue()) 271 | # we remove all the old capabilities for the sender 272 | tempDirectory = defaultdict(list) 273 | for keyword in self._directory: 274 | tempDirectory[keyword] = [cap for cap in self._directory[keyword] 275 | if not senderIdentity.match(Name(cap['name']))] 276 | 277 | # then we add the ones from the message 278 | for capability in message.capabilities: 279 | capabilityPrefix = Name() 280 | for component in capability.commandPrefix.components: 281 | capabilityPrefix.append(component) 282 | commandUri = capabilityPrefix.toUri() 283 | if not senderIdentity.match(capabilityPrefix): 284 | self.log.error("Node {} tried to register another prefix: {} - ignoring update".format( 285 | senderIdentity.toUri(),commandUri)) 286 | else: 287 | for keyword in capability.keywords: 288 | allUris = [info['name'] for info in tempDirectory[keyword]] 289 | if capabilityPrefix not in allUris: 290 | listing = {'signed':capability.needsSignature, 291 | 'name':commandUri} 292 | tempDirectory[keyword].append(listing) 293 | self._directory= tempDirectory 294 | 295 | def _prepareCapabilitiesList(self, interestName): 296 | """ 297 | Responds to a directory listing request with JSON 298 | """ 299 | 300 | dataName = Name(interestName).append(Name.Component.fromNumber(int(time.time()))) 301 | response = Data(dataName) 302 | 303 | response.setContent(json.dumps(self._directory)) 304 | 305 | return response 306 | 307 | ##### 308 | # Interest handling 309 | #### 310 | 311 | def _onCommandReceived(self, prefix, interest, transport, prefixId): 312 | """ 313 | """ 314 | interestName = interest.getName() 315 | 316 | #if it is a certificate name, serve the certificate 317 | foundCert = self._identityStorage.getCertificate(interestName) 318 | if foundCert is not None: 319 | self.log.debug("Serving certificate request") 320 | transport.send(foundCert.wireEncode().buf()) 321 | return 322 | 323 | afterPrefix = interestName.get(prefix.size()).toEscapedString() 324 | if afterPrefix == "listCommands": 325 | #compose device list 326 | self.log.debug("Received device list request") 327 | response = self._prepareCapabilitiesList(interestName) 328 | self.sendData(response, transport) 329 | elif afterPrefix == "certificateRequest": 330 | #build and sign certificate 331 | self.log.debug("Received certificate request") 332 | self._handleCertificateRequest(interest, transport) 333 | elif afterPrefix == "updateCapabilities": 334 | # needs to be signed! 335 | self.log.debug("Received capabilities update") 336 | def onVerifiedCapabilities(interest): 337 | response = Data(interest.getName()) 338 | response.setContent(str(time.time())) 339 | self.sendData(response, transport) 340 | self._updateDeviceCapabilities(interest) 341 | self._keyChain.verifyInterest(interest, 342 | onVerifiedCapabilities, self.verificationFailed) 343 | elif afterPrefix == "addDevice": 344 | self.log.debug("Received pairing request") 345 | def onVerifiedPairingRequest(interest): 346 | response = Data(interest.getName()) 347 | encryptedMessage = interest.getName()[len(prefix)+1] 348 | responseCode = self._beginPairing(encryptedMessage) 349 | response.setContent(str(responseCode)) 350 | self.sendData(response, transport) 351 | self._keyChain.verifyInterest(interest, onVerifiedPairingRequest, self.verificationFailed) 352 | else: 353 | response = Data(interest.getName()) 354 | response.setContent("500") 355 | response.getMetaInfo().setFreshnessPeriod(1000) 356 | transport.send(response.wireEncode().buf()) 357 | 358 | def onStartup(self): 359 | # begin taking add requests 360 | self.log.info('Controller is ready') 361 | 362 | def _showRSAProgress(self, displayStr=''): 363 | displayStr = displayStr.strip() 364 | msg = '' 365 | if displayStr == 'p,q': 366 | msg = 'Generating giant prime numbers...' 367 | elif displayStr == 'd': 368 | msg = 'Generating private exponent...' 369 | elif displayStr == 'u': 370 | msg = 'Checking CRT coefficient...' 371 | self.log.debug(msg) 372 | 373 | 374 | if __name__ == '__main__': 375 | import sys 376 | import os 377 | # todo - should I enforce the suffix 'gateway'? 378 | nArgs = len(sys.argv) - 1 379 | if nArgs < 2: 380 | from pyndn.util.boost_info_parser import BoostInfoParser 381 | fileName = '/usr/local/etc/ndn/iot_controller.conf' 382 | if nArgs == 1: 383 | fileName = sys.argv[1] 384 | 385 | try: 386 | config = BoostInfoParser() 387 | config.read(fileName) 388 | except IOError: 389 | print('Could not read {}, exiting...'.format(fileName)) 390 | sys.exit(1) 391 | else: 392 | deviceName = config["device/controllerName"][0].value 393 | networkName = config["device/environmentPrefix"][0].value 394 | elif nArgs == 2: 395 | networkName = sys.argv[1] 396 | deviceName = sys.argv[2] 397 | else: 398 | print('Usage: {} [network-name controller-name]'.format(sys.argv[0])) 399 | sys.exit(1) 400 | 401 | try: 402 | 403 | tempFile = '/tmp/ndn-iot/iot.conf' 404 | tempDir = os.path.dirname(tempFile) 405 | 406 | if os.path.exists(tempFile): 407 | print ('Another gateway instance may be running. If not, delete {} and try again'.format(tempFile)) 408 | sys.exit(1) 409 | else: 410 | try: 411 | os.makedirs(tempDir) 412 | except OSError: 413 | pass # errors happen when the directory exists :/ 414 | # save configuration in a place where the console can read it 415 | with open(tempFile, 'w') as nameFile: 416 | nameFile.write('{}\t{}\n'.format(networkName, deviceName)) 417 | except IOError as e: 418 | print('Could not write configuration: error') 419 | sys.exit(1) 420 | 421 | deviceSuffix = Name(deviceName) 422 | networkPrefix = Name(networkName) 423 | n = IotController(deviceSuffix, networkPrefix) 424 | try: 425 | n.start() 426 | finally: 427 | os.remove(tempFile) 428 | -------------------------------------------------------------------------------- /ndn_pi/security/.default.conf: -------------------------------------------------------------------------------- 1 | validator 2 | { 3 | rule 4 | { 5 | id "Certificate Trust" 6 | for "data" 7 | filter 8 | { 9 | type "name" 10 | regex "[^]+<>*" 11 | } 12 | checker 13 | { 14 | type "customized" 15 | sig-type "rsa-sha256" 16 | key-locator 17 | { 18 | type "name" 19 | name "/home" 20 | relation "is-strict-prefix-of" 21 | } 22 | } 23 | } 24 | rule 25 | { 26 | id "All Other Data" 27 | for "data" 28 | checker 29 | { 30 | type "hierarchical" 31 | sig-type "rsa-sha256" 32 | } 33 | } 34 | rule 35 | { 36 | id "Command Interests" 37 | for "interest" 38 | filter 39 | { 40 | type "name" 41 | name "/home/default" 42 | relation "is-strict-prefix-of" 43 | } 44 | checker 45 | { 46 | type "customized" 47 | sig-type "rsa-sha256" 48 | key-locator 49 | { 50 | type "name" 51 | name "/home" 52 | relation "is-strict-prefix-of" 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ndn_pi/security/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['iot_identity_manager', 'iot_identity_storage', 'iot_policy_manager', 'iot_private_key_storage', 'hmac_helper'] 2 | 3 | from iot_identity_manager import IotIdentityManager 4 | from iot_identity_storage import IotIdentityStorage 5 | from iot_policy_manager import IotPolicyManager 6 | from iot_private_key_storage import IotPrivateKeyStorage 7 | from hmac_helper import HmacHelper 8 | 9 | -------------------------------------------------------------------------------- /ndn_pi/security/hmac_helper.py: -------------------------------------------------------------------------------- 1 | # -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */ 2 | # 3 | # Copyright (C) 2014 Regents of the University of California. 4 | # Author: Adeola Bannis 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Lesser General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # A copy of the GNU General Public License is in the file COPYING. 19 | from pyndn.encoding import WireFormat 20 | from pyndn.util import Blob 21 | from sha256_hmac_signature import Sha256HmacSignature 22 | from pyndn import Data, KeyLocatorType, Interest, Name 23 | from hashlib import sha256 24 | from random import SystemRandom 25 | from time import time as timestamp 26 | 27 | import hmac 28 | 29 | class HmacHelper(object): 30 | def __init__(self, raw_key, wireFormat=None): 31 | super(HmacHelper, self).__init__() 32 | self.key = sha256(raw_key).digest() 33 | self.random = SystemRandom() 34 | if wireFormat is None: 35 | self.wireFormat = WireFormat.getDefaultWireFormat() 36 | else: 37 | self.wireFormat = wireFormat 38 | 39 | @classmethod 40 | def generatePin(cls): 41 | """ 42 | Generate a pin to be entered into another device. 43 | Restricting this to 8 bytes (16 hex chars) for now. 44 | """ 45 | pin = bytearray(8) 46 | random = SystemRandom() 47 | for i in range(8): 48 | pin[i] = random.randint(0,0xff) 49 | return str(pin).encode('hex') 50 | 51 | @classmethod 52 | def extractInterestSignature(cls, interest, wireFormat=None): 53 | if wireFormat is None: 54 | wireFormat = WireFormat.getDefaultWireFormat() 55 | 56 | try: 57 | signature = wireFormat.decodeSignatureInfoAndValue( 58 | interest.getName().get(-2).getValue().buf(), 59 | interest.getName().get(-1).getValue().buf()) 60 | except: 61 | signature = None 62 | 63 | return signature 64 | 65 | def signData(self, data, keyName=None, wireFormat=None): 66 | data.setSignature(Sha256HmacSignature()) 67 | s = data.getSignature() 68 | 69 | s.getKeyLocator().setType(KeyLocatorType.KEYNAME) 70 | s.getKeyLocator().setKeyName(keyName) 71 | 72 | if wireFormat is None: 73 | wireFormat = WireFormat.getDefaultWireFormat() 74 | encoded = data.wireEncode(wireFormat) 75 | signer = hmac.new(self.key, bytearray(encoded.toSignedBuffer()), sha256) 76 | s.setSignature(Blob(signer.digest())) 77 | data.wireEncode(wireFormat) 78 | 79 | def verifyData(self, data, wireFormat=None): 80 | # clear out old signature so encoding does not include it 81 | if wireFormat is None: 82 | wireFormat = WireFormat.getDefaultWireFormat() 83 | encoded = data.wireEncode(wireFormat) 84 | hasher = hmac.new(self.key, bytearray(encoded.toSignedBuffer()), sha256) 85 | sigBytes = data.getSignature().getSignature() 86 | return sigBytes.toRawStr() == hasher.digest() 87 | 88 | def signInterest(self, interest, keyName=None, wireFormat=None): 89 | # Adds the nonce and timestamp here, because there is no 90 | # 'makeCommandInterest' call for this yet 91 | nonceValue = bytearray(8) 92 | for i in range(8): 93 | nonceValue[i] = self.random.randint(0,0xff) 94 | timestampValue = bytearray(8) 95 | ts = int(timestamp()*1000) 96 | for i in range(8): 97 | byte = ts & 0xff 98 | timestampValue[-(i+1)] = byte 99 | ts = ts >> 8 100 | 101 | if wireFormat is None: 102 | wireFormat = WireFormat.getDefaultWireFormat() 103 | 104 | s = Sha256HmacSignature() 105 | s.getKeyLocator().setType(KeyLocatorType.KEYNAME) 106 | s.getKeyLocator().setKeyName(keyName) 107 | 108 | interestName = interest.getName() 109 | interestName.append(nonceValue).append(timestampValue) 110 | interestName.append(wireFormat.encodeSignatureInfo(s)) 111 | interestName.append(Name.Component()) 112 | 113 | encoding = interest.wireEncode(wireFormat) 114 | signer = hmac.new(self.key, encoding.toSignedBuffer(), sha256) 115 | 116 | s.setSignature(Blob(signer.digest())) 117 | interest.setName(interestName.getPrefix(-1).append( 118 | wireFormat.encodeSignatureValue(s))) 119 | 120 | 121 | def verifyInterest(self, interest, wireFormat=None): 122 | if wireFormat is None: 123 | wireFormat = WireFormat.getDefaultWireFormat() 124 | 125 | signature = self.extractInterestSignature(interest, wireFormat) 126 | encoding = interest.wireEncode(wireFormat) 127 | hasher = hmac.new(self.key, encoding.toSignedBuffer(), sha256) 128 | return signature.getSignature().toRawStr() == hasher.digest() 129 | 130 | -------------------------------------------------------------------------------- /ndn_pi/security/iot_identity_manager.py: -------------------------------------------------------------------------------- 1 | # -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */ 2 | # 3 | # Copyright (C) 2014 Regents of the University of California. 4 | # Author: Adeola Bannis 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Lesser General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # A copy of the GNU General Public License is in the file COPYING. 19 | 20 | from pyndn.security.identity import IdentityManager 21 | from pyndn.util.common import Common 22 | from pyndn.util import Blob 23 | from pyndn.name import Name 24 | from iot_private_key_storage import IotPrivateKeyStorage 25 | from Crypto.PublicKey import RSA 26 | from Crypto.Cipher import PKCS1_OAEP 27 | from pyndn.security.security_types import KeyType 28 | from pyndn.security.certificate import IdentityCertificate, PublicKey, CertificateSubjectDescription 29 | from pyndn.security.security_exception import SecurityException 30 | import struct 31 | 32 | class IotIdentityManager(IdentityManager): 33 | """ 34 | Overrides the default constructor to force the use of our 35 | IotPrivateKeyStorage 36 | """ 37 | def __init__(self, identityStorage=None): 38 | super(IotIdentityManager, self).__init__(identityStorage, IotPrivateKeyStorage()) 39 | 40 | def getPrivateKey(self, keyName): 41 | return self._privateKeyStorage.getPrivateKey(keyName) 42 | 43 | def addPrivateKey(self, keyName, keyDer): 44 | self._privateKeyStorage.addPrivateKey(keyName, keyDer) 45 | 46 | def _getNewKeyBits(self, keySize, progress_func=None): 47 | # returns public and private key DER in blobs 48 | key = RSA.generate(keySize, progress_func=progress_func) 49 | publicDer = key.publickey().exportKey(format='DER') 50 | privateDer = key.exportKey(format='DER', pkcs=8) 51 | return (Blob(publicDer, False), Blob(privateDer, False)) 52 | 53 | 54 | def generateRSAKeyPair(self, identityName, isKsk=False, keySize=2048, progressFunc=None): 55 | """ 56 | Generate a pair of RSA keys for the specified identity. 57 | 58 | :param Name identityName: The name of the identity. 59 | :param bool isKsk: (optional) true for generating a Key-Signing-Key 60 | (KSK), false for a Data-Signing-Key (DSK). If omitted, generate a 61 | Data-Signing-Key. 62 | :param int keySize: (optional) The size of the key. If omitted, use a 63 | default secure key size. 64 | :param function progressFunc: An update function taking a string 65 | argument. See PyCrypto's RSA.generate for more information. 66 | :return: The generated key name. 67 | :rtype: Name 68 | """ 69 | keyName = self._identityStorage.getNewKeyName(identityName, isKsk) 70 | publicBits, privateBits = self._getNewKeyBits(keySize, progressFunc) 71 | self._identityStorage.addKey(keyName, KeyType.RSA, publicBits) 72 | self._privateKeyStorage.addPrivateKey(keyName, privateBits) 73 | 74 | return keyName 75 | 76 | 77 | def generateRSAKeyPairAsDefault(self, identityName, isKsk=False, keySize=2048, progressFunc=None): 78 | """ 79 | Generate a pair of RSA keys for the specified identity and set it as 80 | default key for the identity. 81 | 82 | :param NameidentityName: The name of the identity. 83 | :param bool isKsk: (optional) true for generating a Key-Signing-Key 84 | (KSK), false for a Data-Signing-Key (DSK). If omitted, generate a 85 | Data-Signing-Key. 86 | :param int keySize: (optional) The size of the key. If omitted, use a 87 | default secure key size. 88 | :param function progressFunc: An update function taking a string 89 | argument. See PyCrypto's RSA.generate for more information 90 | :return: The generated key name. 91 | :rtype: Name 92 | """ 93 | newKeyName = self.generateRSAKeyPair(identityName, isKsk, keySize, progressFunc) 94 | self._identityStorage.setDefaultKeyNameForIdentity(newKeyName) 95 | return newKeyName 96 | 97 | def selfSign(self, keyName): 98 | """ 99 | Generate a self-signed certificate for a public key. 100 | 101 | :param Name keyName: The name of the public key. 102 | :return: The generated certificate. 103 | :rtype: IdentityCertificate 104 | """ 105 | certificate = self.generateCertificateForKey(keyName) 106 | self.signByCertificate(certificate, certificate.getName()) 107 | 108 | return certificate 109 | 110 | def generateCertificateForKey(self, keyName): 111 | # let any raised SecurityExceptions bubble up 112 | publicKeyBits = self._identityStorage.getKey(keyName) 113 | publicKeyType = self._identityStorage.getKeyType(keyName) 114 | 115 | publicKey = PublicKey(publicKeyType, publicKeyBits) 116 | 117 | timestamp = Common.getNowMilliseconds() 118 | 119 | # TODO: specify where the 'KEY' component is inserted 120 | # to delegate responsibility for cert delivery 121 | certificateName = keyName.getPrefix(-1).append('KEY').append(keyName.get(-1)) 122 | certificateName.append("ID-CERT").append(Name.Component(struct.pack(">Q", timestamp))) 123 | 124 | certificate = IdentityCertificate(certificateName) 125 | 126 | 127 | certificate.setNotBefore(timestamp) 128 | certificate.setNotAfter((timestamp + 30*86400*1000)) # about a month 129 | 130 | certificate.setPublicKeyInfo(publicKey) 131 | 132 | # ndnsec likes to put the key name in a subject description 133 | sd = CertificateSubjectDescription("2.5.4.41", keyName.toUri()) 134 | certificate.addSubjectDescription(sd) 135 | 136 | certificate.encode() 137 | 138 | return certificate 139 | 140 | def encryptForIdentity(self, plaintext, identityName=None, keyName=None): 141 | """ 142 | Encrypt the given bytes using the default key for the given identity, 143 | or the specfied key. If the key name is specified, the identity name is ignored. 144 | If there is no key or identity name given, a SecurityException is raised. 145 | :param plaintext: The data to encrypt 146 | :type plaintext: Blob 147 | :param Name identityName: (optional) The identity who will decrypt the message. 148 | :param Name keyName: (optional) The key used to encrypt the message. 149 | :return: The encrypted data 150 | :rtype: Blob 151 | """ 152 | if keyName is None: 153 | keyName = self.getDefaultKeyNameForIdentity(identityName) 154 | keyDer = self._identityStorage.getKey(keyName) 155 | 156 | key = RSA.importKey(str(keyDer)) 157 | cipher = PKCS1_OAEP.new(key) 158 | 159 | encrypted = Blob(cipher.encrypt(str(plaintext)), False) 160 | 161 | return encrypted 162 | 163 | def decryptAsIdentity(self, ciphertext, identityName=None, keyName=None): 164 | """ 165 | Decrypt the given bytes using the default key for the given identity, 166 | or the specfied key. If the key name is specified, the identity name is ignored. 167 | If there is no key or identity name given, a SecurityException is raised. 168 | :param ciphertext: The data to decrypt 169 | :type ciphertext: Blob 170 | :param Name identityName: (optional) The identity the message is intended for. 171 | :param Name keyName: (optional) The key used to decrypt the message. 172 | :return: The decrypted data 173 | :rtype: Blob 174 | """ 175 | if keyName is None: 176 | keyName = self.getDefaultKeyNameForIdentity(identityName) 177 | keyDer = self.getPrivateKey(keyName) 178 | key = RSA.importKey(str(keyDer)) 179 | cipher = PKCS1_OAEP.new(key) 180 | 181 | decrypted = Blob(cipher.decrypt(str(ciphertext)), False) 182 | 183 | return decrypted 184 | 185 | -------------------------------------------------------------------------------- /ndn_pi/security/iot_identity_storage.py: -------------------------------------------------------------------------------- 1 | # -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */ 2 | # 3 | # Copyright (C) 2014 Regents of the University of California. 4 | # Author: Adeola Bannis 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Lesser General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # A copy of the GNU General Public License is in the file COPYING. 19 | 20 | """ 21 | This module is based on the BasicIdentityStorage class 22 | """ 23 | from pyndn.util import Blob 24 | from pyndn.security.certificate import IdentityCertificate 25 | from pyndn.security.security_exception import SecurityException 26 | from pyndn import Name, Data 27 | from pyndn.security.identity.basic_identity_storage import BasicIdentityStorage 28 | import base64 29 | 30 | class IotIdentityStorage(BasicIdentityStorage): 31 | def addIdentity(self, identityName): 32 | """ 33 | Add a new identity. An exception will be thrown if the identity already 34 | exists. 35 | 36 | :param Name identityName: The identity name. 37 | """ 38 | identityUri = identityName.toUri() 39 | if self.doesIdentityExist(identityName): 40 | raise SecurityException("The identity {} already exists".format( 41 | identityUri)) 42 | 43 | cursor = self._database.cursor() 44 | cursor.execute("INSERT INTO Identity(identity_name) VALUES(?)", 45 | (identityUri,)) 46 | self._database.commit() 47 | cursor.close() 48 | 49 | def addKey(self, keyName, keyType, publicKeyDer): 50 | """ 51 | Add a public key to the identity storage. 52 | 53 | :param Name keyName: The name of the public key to be added. 54 | :param keyType: Type of the public key to be added. 55 | :type keyType: int from KeyType 56 | :param Blob publicKeyDer: A blob of the public key DER to be added. 57 | """ 58 | if self.doesKeyExist(keyName): 59 | raise SecurityException("A key with the same name already exists!") 60 | 61 | identityName = keyName.getPrefix(-1) 62 | identityUri = identityName.toUri() 63 | makeDefault = 0 64 | if not self.doesIdentityExist(identityName): 65 | self.addIdentity(identityName) 66 | makeDefault = 1 67 | 68 | keyId = keyName.get(-1).toEscapedString() 69 | keyBuffer = buffer(bytearray (publicKeyDer.buf())) 70 | 71 | cursor = self._database.cursor() 72 | cursor.execute("INSERT INTO Key VALUES(?,?,?,?,?, ?)", 73 | (identityUri, keyId, keyType, keyBuffer, makeDefault, 1)) 74 | self._database.commit() 75 | cursor.close() 76 | 77 | def getKey(self, keyName): 78 | """ 79 | Get the public key DER blob from the identity storage. 80 | 81 | :param Name keyName: The name of the requested public key. 82 | :return: The DER Blob. If not found, return a isNull() Blob. 83 | :rtype: Blob 84 | """ 85 | identityUri = keyName.getPrefix(-1).toUri() 86 | keyId = keyName.get(-1).toEscapedString() 87 | 88 | cursor = self._database.cursor() 89 | cursor.execute("SELECT public_key FROM Key WHERE identity_name=? AND key_identifier=?", 90 | (identityUri, keyId)) 91 | (keyData, ) = cursor.fetchone() 92 | return Blob(bytearray(keyData)) 93 | 94 | def doesCertificateExist(self, certificateName): 95 | """ 96 | Check if the specified certificate already exists. 97 | 98 | :param Name certificateName: The name of the certificate. 99 | :return: True if the certificate exists, otherwise False. 100 | :rtype: bool 101 | """ 102 | cursor = self._database.cursor() 103 | # need to use LIKE because key locators cut off timestamps 104 | escapedUri = certificateName.toUri().replace('%', '\\%') 105 | cursor.execute( 106 | "SELECT count(*) FROM Certificate WHERE cert_name LIKE ? ESCAPE '\\'", 107 | (escapedUri+'%',)) 108 | certExists = False 109 | (count,) = cursor.fetchone() 110 | if count > 0: 111 | certExists = True 112 | 113 | cursor.close() 114 | return certExists 115 | 116 | def addCertificate(self, certificate): 117 | """ 118 | Add a certificate to the identity storage. 119 | 120 | :param IdentityCertificate certificate: The certificate to be added. 121 | This makes a copy of the certificate. 122 | """ 123 | #TODO: actually check validity of certificate timestamp 124 | certificateName = certificate.getName() 125 | 126 | if self.doesCertificateExist(certificateName): 127 | raise SecurityException("Certificate has already been installed!") 128 | 129 | certCopy = IdentityCertificate(certificate) 130 | makeDefault = 0 131 | keyName = certCopy.getPublicKeyName() 132 | keyInfo = certCopy.getPublicKeyInfo() 133 | if not self.doesKeyExist(keyName): 134 | self.addKey(keyName, keyInfo.getKeyType(), keyInfo.getKeyDer()) 135 | makeDefault = 1 136 | else: 137 | # see if the key we already have matches this certificate 138 | keyBlob = self.getKey(keyName) 139 | if (keyBlob.isNull() or keyBlob.toBuffer() != 140 | keyInfo.getKeyDer().toBuffer()): 141 | raise SecurityException("Certificate does not match public key") 142 | 143 | keyId = keyName.get(-1).toEscapedString() 144 | identityUri = keyName.getPrefix(-1).toUri() 145 | certIssuer = certCopy.getSignature().getKeyLocator().getKeyName().toUri() 146 | encodedCert = buffer(bytearray(certCopy.wireEncode().buf())) 147 | notBefore = certCopy.getNotBefore() 148 | notAfter = certCopy.getNotAfter() 149 | cursor = self._database.cursor() 150 | cursor.execute("INSERT INTO Certificate VALUES(?,?,?,?,?,?,?,?,?)", 151 | (certificateName.toUri(), certIssuer, identityUri, keyId, 152 | notBefore, notAfter, encodedCert, 1, makeDefault)) 153 | self._database.commit() 154 | cursor.close() 155 | 156 | 157 | def getCertificate(self, certificateName, allowAny = False): 158 | """ 159 | Get a certificate from the identity storage. 160 | 161 | :param Name certificateName: The name of the requested certificate. 162 | :param bool allowAny: (optional) If False, only a valid certificate will 163 | be returned, otherwise validity is disregarded. If omitted, 164 | allowAny is False. 165 | :return: The requested certificate. If not found, return None. 166 | :rtype: Data 167 | """ 168 | chosenCert = None 169 | certificateUri = certificateName.toUri() 170 | cursor = self._database.cursor() 171 | 172 | #if not allowAny: 173 | # validityClause = " AND valid_flag=1" 174 | #else: 175 | validityClause = "" 176 | 177 | # use LIKE because key locators chop off timestamps 178 | # need to escape any percent signs in the certificate uri for sql's 179 | # sake, but still append % for LIKE 180 | escapedUri = certificateUri.replace('%', '\\%') 181 | full_statement = "SELECT certificate_data FROM Certificate WHERE cert_name LIKE ?"+validityClause+" ESCAPE '\\' ORDER BY cert_name DESC" 182 | #full_statement = "SELECT certificate_data FROM Certificate WHERE cert_name=?"+validityClause 183 | cursor.execute(full_statement, (escapedUri+'%', )) 184 | try: 185 | (certData, ) = cursor.fetchone() 186 | except TypeError: 187 | pass 188 | else: 189 | chosenCert = IdentityCertificate() 190 | chosenCert.wireDecode(bytearray(certData)) 191 | return chosenCert 192 | 193 | def setDefaultIdentity(self, identityName): 194 | """ 195 | Set the default identity. If the identityName does not exist, 196 | raises a SecurityException. 197 | 198 | :param Name identityName: The default identity name. 199 | """ 200 | if not self.doesIdentityExist(identityName): 201 | raise SecurityException("Identity does not exist") 202 | 203 | try: 204 | cursor = None 205 | currentDefault = self.getDefaultIdentity().toUri() 206 | except SecurityException: 207 | # no default, no need to remove default flag 208 | pass 209 | else: 210 | cursor = self._database.cursor() 211 | cursor.execute("UPDATE Identity SET default_identity=0 WHERE identity_name=?", (currentDefault,)) 212 | 213 | if cursor is None: 214 | cursor = self._database.cursor() 215 | 216 | # now set this identity as default 217 | cursor.execute("UPDATE Identity SET default_identity=1 WHERE identity_name=?", (identityName.toUri(), )) 218 | 219 | self._database.commit() 220 | cursor.close() 221 | 222 | def setDefaultKeyNameForIdentity(self, keyName, identityNameCheck = None): 223 | """ 224 | Set the default key name for the corresponding identity. 225 | :param Name keyName: The key name. 226 | :param Name identityNameCheck: Not used 227 | """ 228 | 229 | if not self.doesKeyExist(keyName): 230 | raise SecurityException("Key does not exist") 231 | 232 | keyId = keyName.get(-1).toEscapedString() 233 | if identityNameCheck is None: 234 | identityName = keyName.getPrefix(-1) 235 | else: 236 | identityName = identityNameCheck 237 | 238 | identityUri = identityName.toUri() 239 | try: 240 | cursor = None 241 | currentDefault = self.getDefaultKeyNameForIdentity(identityName) 242 | except SecurityException: 243 | # no current default, it's okay 244 | pass 245 | else: 246 | cursor = self._database.cursor() 247 | currentKeyId = currentDefault.get(-1).toEscapedString() 248 | cursor.execute("UPDATE Key SET default_key=0 WHERE identity_name=? AND key_identifier=?", 249 | (identityUri, currentKeyId)) 250 | 251 | if cursor is None: 252 | cursor = self._database.cursor() 253 | 254 | cursor.execute("UPDATE Key SET default_key=1 WHERE identity_name=? AND key_identifier=?", (identityUri, keyId)) 255 | 256 | self._database.commit() 257 | cursor.close() 258 | 259 | def setDefaultCertificateNameForKey(self, keyName, certificateName): 260 | """ 261 | Set the default certificate name for the corresponding key 262 | 263 | :param Name keyName: not used 264 | :param Name certificateName: The certificate name. 265 | """ 266 | 267 | if not self.doesCertificateExist(certificateName): 268 | raise SecurityException("Certificate does not exist") 269 | 270 | keyName = IdentityCertificate.certificateNameToPublicKeyName(certificateName) 271 | identityUri = keyName.getPrefix(-1).toUri() 272 | keyId = keyName.get(-1).toEscapedString() 273 | 274 | try: 275 | cursor = None 276 | currentDefault = self.getDefaultCertificateNameForKey(keyName) 277 | except SecurityException: 278 | pass 279 | else: 280 | cursor = self._database.cursor() 281 | cursor.execute("UPDATE Certificate SET default_cert=0 WHERE cert_name=? AND identity_name=? AND key_identifier=?", 282 | (currentDefault.toUri(), identityUri, keyId)) 283 | 284 | if cursor is None: 285 | cursor = self._database.cursor() 286 | 287 | cursor.execute("UPDATE Certificate SET default_cert=1 WHERE cert_name=? AND identity_name=? AND key_identifier=?", 288 | (certificateName.toUri(), identityUri, keyId)) 289 | 290 | self._database.commit() 291 | cursor.close() 292 | -------------------------------------------------------------------------------- /ndn_pi/security/iot_policy_manager.py: -------------------------------------------------------------------------------- 1 | # -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */ 2 | # 3 | # Copyright (C) 2014 Regents of the University of California. 4 | # Author: Adeola Bannis 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Lesser General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # A copy of the GNU General Public License is in the file COPYING. 19 | 20 | 21 | import sys 22 | 23 | from pyndn.security.policy import ConfigPolicyManager 24 | from pyndn import Name 25 | 26 | from pyndn.security.security_exception import SecurityException 27 | from pyndn.util.boost_info_parser import BoostInfoParser 28 | 29 | import os 30 | 31 | """ 32 | This module implements a simple hierarchical trust model that uses certificate 33 | data to determine whether another signature/name can be trusted. 34 | 35 | The policy manager enforces an environment, which corresponds to the network 36 | prefix, i.e. the root of the network namespace. 37 | All command interests must be signed with a certificate in this environment 38 | to be trusted. 39 | 40 | There is a root name and public key which must be the top authority in the environment 41 | for the certificate to be trusted. 42 | """ 43 | 44 | class IotPolicyManager(ConfigPolicyManager): 45 | def __init__(self, identityStorage, configFilename=None): 46 | """ 47 | :param pyndn.IdentityStorage: A class that stores signing identities and certificates. 48 | :param str configFilename: A configuration file specifying validation rules and network 49 | name settings. 50 | """ 51 | 52 | # use the default configuration where possible 53 | # TODO: use environment variable for this, fall back to default 54 | path = os.path.dirname(__file__) 55 | templateFilename = os.path.join(path, '.default.conf') 56 | self._configTemplate = BoostInfoParser() 57 | self._configTemplate.read(templateFilename) 58 | 59 | if configFilename is None: 60 | configFilename = templateFilename 61 | 62 | super(IotPolicyManager, self).__init__(identityStorage, configFilename) 63 | self.setEnvironmentPrefix(None) 64 | self.setTrustRootIdentity(None) 65 | self.setDeviceIdentity(None) 66 | 67 | def updateTrustRules(self): 68 | """ 69 | Should be called after either the device identity, trust root or network 70 | prefix is changed. 71 | 72 | Not called automatically in case they are all changing (typical for 73 | bootstrapping). 74 | 75 | Resets the validation rules if we don't have a trust root or enivronment 76 | 77 | """ 78 | validatorTree = self._configTemplate["validator"][0].clone() 79 | 80 | if (self._environmentPrefix.size() > 0 and 81 | self._trustRootIdentity.size() > 0 and 82 | self._deviceIdentity.size() > 0): 83 | # don't sneak in a bad identity 84 | if not self._environmentPrefix.match(self._deviceIdentity): 85 | raise SecurityException("Device identity does not belong to configured network!") 86 | 87 | environmentUri = self._environmentPrefix.toUri() 88 | deviceUri = self._deviceIdentity.toUri() 89 | 90 | for rule in validatorTree["rule"]: 91 | ruleId = rule["id"][0].value 92 | if ruleId == 'Certificate Trust': 93 | #modify the 'Certificate Trust' rule 94 | rule["checker/key-locator/name"][0].value = environmentUri 95 | elif ruleId == 'Command Interests': 96 | rule["filter/name"][0].value = deviceUri 97 | rule["checker/key-locator/name"][0].value = environmentUri 98 | 99 | #remove old validation rules from config 100 | # replace with new validator rules 101 | self.config._root.subtrees["validator"] = [validatorTree] 102 | 103 | 104 | def inferSigningIdentity(self, fromName): 105 | """ 106 | Used to map Data or Interest names to identitites. 107 | :param pyndn.Name fromName: The name of a Data or Interest packet 108 | """ 109 | # works if you have an IotIdentityStorage 110 | return self._identityStorage.inferIdentityForName(fromName) 111 | 112 | def setTrustRootIdentity(self, identityName): 113 | """ 114 | : param pyndn.Name identityName: The new identity to trust as the controller. 115 | """ 116 | self._trustRootIdentity = Name(identityName) 117 | 118 | def getTrustRootIdentity(self): 119 | """ 120 | : return pyndn.Name: The trusted controller's network name. 121 | """ 122 | return Name(self._trustRootIdentity) 123 | 124 | def setEnvironmentPrefix(self, name): 125 | """ 126 | : param pyndn.Name name: The new root of the network namespace (network prefix) 127 | """ 128 | self._environmentPrefix = Name(name) 129 | 130 | def getEnvironmentPrefix(self): 131 | """ 132 | :return: The root of the network namespace 133 | :rtype: pyndn.Name 134 | """ 135 | return Name(self._environmentPrefix) 136 | 137 | def getDeviceIdentity(self): 138 | return self._deviceIdentity 139 | 140 | def setDeviceIdentity(self, identity): 141 | self._deviceIdentity = Name(identity) 142 | 143 | def hasRootCertificate(self): 144 | """ 145 | :return: Whether we've downloaded the controller's network certificate 146 | :rtype: boolean 147 | """ 148 | try: 149 | rootCertName = self._identityStorage.getDefaultCertificateNameForIdentity( 150 | self._trustRootIdentity) 151 | except SecurityException: 152 | return False 153 | 154 | try: 155 | rootCert = self._identityStorage.getCertificate(rootCertName) 156 | if rootCert is not None: 157 | return True 158 | finally: 159 | return False 160 | 161 | def hasRootSignedCertificate(self): 162 | """ 163 | :return: Whether we've received a network certificate from our controller 164 | :rtype: boolean 165 | """ 166 | try: 167 | myCertName = self._identityStorage.getDefaultCertificateNameForIdentity( 168 | self._deviceIdentity) 169 | myCert = self._identityStorage.getCertificate(myCertName) 170 | if self._trustRootIdentity.match( 171 | myCert.getSignature().getKeyLocator().getKeyName()): 172 | return True 173 | except SecurityException: 174 | pass 175 | 176 | return False 177 | 178 | def removeTrustRules(self): 179 | """ 180 | Resets the network prefix, device identity and trust root identity to 181 | empty values 182 | """ 183 | self.setDeviceIdentity(None) 184 | self.setTrustRootIdentity(None) 185 | self.setEnvironmentPrefix(None) 186 | self.updateTrustRules() 187 | -------------------------------------------------------------------------------- /ndn_pi/security/iot_private_key_storage.py: -------------------------------------------------------------------------------- 1 | # -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */ 2 | # 3 | # Copyright (C) 2014 Regents of the University of California. 4 | # Author: Adeola Bannis 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Lesser General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # A copy of the GNU General Public License is in the file COPYING. 19 | 20 | """ 21 | This module defines the IotPrivateKeyStorage class which extends 22 | FilePrivateKeyStorage to implement private key storage using files. 23 | """ 24 | 25 | import os 26 | import sys 27 | import base64 28 | from Crypto.Hash import SHA256 29 | from Crypto.PublicKey import RSA 30 | from Crypto.Signature import PKCS1_v1_5 31 | from pyndn.util import Blob 32 | from pyndn.security.security_types import DigestAlgorithm 33 | from pyndn.security.security_types import KeyClass 34 | from pyndn.security.security_types import KeyType 35 | from pyndn.security.security_exception import SecurityException 36 | from pyndn.security.identity.file_private_key_storage import FilePrivateKeyStorage 37 | 38 | class IotPrivateKeyStorage(FilePrivateKeyStorage): 39 | 40 | def sign(self, data, keyName, digestAlgorithm = DigestAlgorithm.SHA256): 41 | """ 42 | Fetch the private key for keyName and sign the data, returning a 43 | signature Blob. 44 | 45 | :param data: Pointer the input byte buffer to sign. 46 | :type data: An array type with int elements 47 | :param Name keyName: The name of the signing key. 48 | :param digestAlgorithm: (optional) the digest algorithm. If omitted, 49 | use DigestAlgorithm.SHA256. 50 | :type digestAlgorithm: int from DigestAlgorithm 51 | :return: The signature, or an isNull() Blob pointer if signing fails. 52 | :rtype: Blob 53 | """ 54 | if digestAlgorithm != DigestAlgorithm.SHA256: 55 | raise SecurityException( 56 | "FilePrivateKeyStorage.sign: Unsupported digest algorithm") 57 | 58 | der = self.getPrivateKey(keyName) 59 | privateKey = RSA.importKey(der.toRawStr()) 60 | 61 | # Sign the hash of the data. 62 | if sys.version_info[0] == 2: 63 | # In Python 2.x, we need a str. Use Blob to convert data. 64 | data = Blob(data, False).toRawStr() 65 | signature = PKCS1_v1_5.new(privateKey).sign(SHA256.new(data)) 66 | # Convert the string to a Blob. 67 | return Blob(bytearray(signature), False) 68 | 69 | def doesKeyExist(self, keyName, keyClass): 70 | """ 71 | Check if a particular key exists. 72 | 73 | :param Name keyName: The name of the key. 74 | :param keyClass: The class of the key, e.g. KeyClass.PUBLIC, 75 | KeyClass.PRIVATE, or KeyClass.SYMMETRIC. 76 | :type keyClass: int from KeyClass 77 | :return: True if the key exists, otherwise false. 78 | :rtype: bool 79 | """ 80 | keyURI = keyName.toUri() 81 | if keyClass == KeyClass.PUBLIC: 82 | return os.path.isfile(self.nameTransform(keyURI, ".pub")) 83 | elif keyClass == KeyClass.PRIVATE: 84 | return os.path.isfile(self.nameTransform(keyURI, ".pri")) 85 | elif keyClass == KeyClass.SYMMETRIC: 86 | return os.path.isfile(self.nameTransform(keyURI, ".key").c_str()) 87 | else: 88 | return False 89 | 90 | def addPrivateKey(self, keyName, keyDer): 91 | """ 92 | Add a private key to the store. 93 | :param Name keyName: The name of the key 94 | :param Blob keyDer: The private key DER 95 | """ 96 | if self.doesKeyExist(keyName, KeyClass.PRIVATE): 97 | raise SecurityException("The private key already exists!") 98 | keyUri = keyName.toUri() 99 | newPath = self.nameTransform(keyUri, ".pri") 100 | 101 | # The private key is generated by NFD which stores as PKCS #8. 102 | # This hack skips the PKCS #8 preamble because PyCryoto decodes as PKCS #1. 103 | # TODO: Use proper PKCS #8 decoding instead of this hack. 104 | 105 | encodedDer = base64.b64encode(keyDer.toRawStr()) 106 | with open(newPath, 'w') as keyFile: 107 | keyFile.write(encodedDer) 108 | 109 | def getPrivateKey(self, keyName): 110 | """ 111 | Fetch a private key from the store. 112 | :param Name keyName: The name of the private key to look up. 113 | :return: The binary DER encoding of the private key bits 114 | :rtype: Blob 115 | """ 116 | keyURI = keyName.toUri() 117 | 118 | if not self.doesKeyExist(keyName, KeyClass.PRIVATE): 119 | raise SecurityException( 120 | "FilePrivateKeyStorage.sign: private key doesn't exist") 121 | 122 | # Read the private key. 123 | base64Content = None 124 | with open(self.nameTransform(keyURI, ".pri")) as keyFile: 125 | base64Content = keyFile.read() 126 | der = base64.b64decode(base64Content) 127 | 128 | return Blob(der) 129 | -------------------------------------------------------------------------------- /ndn_pi/security/sha256_hmac_signature.py: -------------------------------------------------------------------------------- 1 | # -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */ 2 | # 3 | # Copyright (C) 2014 Regents of the University of California. 4 | # Author: Jeff Thompson 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Lesser General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # A copy of the GNU General Public License is in the file COPYING. 19 | 20 | """ 21 | This module defines the Sha256HmacSignature class which extends Signature and 22 | holds the signature bits and other info representing a SHA256-HMAC signature 23 | in a data packet. 24 | """ 25 | 26 | from pyndn.util.change_counter import ChangeCounter 27 | from pyndn.util import Blob 28 | from pyndn.signature import Signature 29 | from pyndn.key_locator import KeyLocator 30 | 31 | class Sha256HmacSignature(Signature): 32 | """ 33 | Create a new Sha256HmacSignature object, possibly copying values from 34 | another object. 35 | 36 | :param value: (optional) If value is a Sha256HmacSignature, copy its 37 | values. If value is omitted, the keyLocator is the default with 38 | unspecified values and the signature is unspecified. 39 | :param value: Sha256HmacSignature 40 | """ 41 | def __init__(self, value = None): 42 | if value == None: 43 | self._keyLocator = ChangeCounter(KeyLocator()) 44 | self._signature = Blob() 45 | elif type(value) is Sha256HmacSignature: 46 | # Copy its values. 47 | self._keyLocator = ChangeCounter(KeyLocator(value.getKeyLocator())) 48 | self._signature = value._signature 49 | else: 50 | raise RuntimeError( 51 | "Unrecognized type for Sha256HmacSignature constructor: " + 52 | repr(type(value))) 53 | 54 | self._changeCount = 0 55 | 56 | def clone(self): 57 | """ 58 | Create a new Sha256HmacSignature which is a copy of this object. 59 | 60 | :return: A new object which is a copy of this object. 61 | :rtype: Sha256HmacSignature 62 | """ 63 | return Sha256HmacSignature(self) 64 | 65 | def getKeyLocator(self): 66 | """ 67 | Get the key locator. 68 | 69 | :return: The key locator. 70 | :rtype: KeyLocator 71 | """ 72 | return self._keyLocator.get() 73 | 74 | def getSignature(self): 75 | """ 76 | Get the data packet's signature bytes. 77 | 78 | :return: The signature bytes as a Blob, which maybe isNull(). 79 | :rtype: Blob 80 | """ 81 | return self._signature 82 | 83 | def setKeyLocator(self, keyLocator): 84 | """ 85 | Set the key locator to a copy of the given keyLocator. 86 | 87 | :param KeyLocator keyLocator: The KeyLocator to copy. 88 | """ 89 | self._keyLocator.set(KeyLocator(keyLocator)) 90 | self._changeCount += 1 91 | 92 | def setSignature(self, signature): 93 | """ 94 | Set the signature bytes to the given value. 95 | 96 | :param signature: The array with the signature bytes. If signature is 97 | not a Blob, then create a new Blob to copy the bytes (otherwise 98 | take another pointer to the same Blob). 99 | :type signature: A Blob or an array type with int elements 100 | """ 101 | self._signature = (signature if type(signature) is Blob 102 | else Blob(signature)) 103 | self._changeCount += 1 104 | 105 | def clear(self): 106 | self._keyLocator.get().clear() 107 | self._signature = Blob() 108 | self._changeCount += 1 109 | 110 | def getChangeCount(self): 111 | """ 112 | Get the change count, which is incremented each time this object 113 | (or a child object) is changed. 114 | 115 | :return: The change count. 116 | :rtype: int 117 | """ 118 | # Make sure each of the checkChanged is called. 119 | changed = self._keyLocator.checkChanged() 120 | if changed: 121 | # A child object has changed, so update the change count. 122 | self._changeCount += 1 123 | 124 | return self._changeCount 125 | --------------------------------------------------------------------------------