Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 26 additions & 28 deletions SystemIntegration.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -658,19 +658,22 @@
"\n",
"The best practice here, whether you are working with ROS, socket code, XML-RPC, LCM, or whatever, is *separation of application and communication*. Specifically, the best thing to do is to keep your shared variables, tasks, and handlers in a single class object that handles all calculations. With such a framework, your class defines the API by which your program operates. You will also be able to develop unit tests and switch communication frameworks more easily when the time comes.\n",
"\n",
"For the above examples, you should define a single class as follows (in a separate file from the main file):\n",
"As a concrete example, consider an application that reads images from another process and extracts a target point from each one, controlling a robot to servo to the extracted point. The extraction process can be configured by some settings, which should also be read from an external process. To implement this functionality, you should define a single class as follows (in a separate file from the main file):\n",
"\n",
"```python\n",
"class MyProgram:\n",
" def __init__(self):\n",
" self.settings = {'some_setting':5}\n",
" self.settings = {'some_setting': 5}\n",
" self.target = None\n",
" def background_task(self):\n",
" print(\"Setting\",self.settings['some_setting'])\n",
" if self.target is not None:\n",
" # Send motor commands to servo to the extracted point\n",
" ... #do something...\n",
" def set_setting(self,new_setting):\n",
" def set_setting(self, new_setting):\n",
" self.settings['some_setting'] = new_setting\n",
" def perform_query(self,data):\n",
" ... #do something...\n",
" def extract_target(self, img: np.ndarray):\n",
" # Run some algorithm to extract `new_target` from `img`\n",
" self.target = new_target\n",
" def done(self):\n",
" ... #do something...\n",
"```\n",
Expand All @@ -682,27 +685,19 @@
"\n",
"program = MyProgram()\n",
"while not program.done():\n",
" data = check_for_input1()\n",
" if data is not None:\n",
" program.set_setting(data)\n",
" data = check_for_input2()\n",
" if data is not None:\n",
" program.perform_query(data)\n",
" program.background_task()\n",
" time.sleep(dt)\n",
"```\n",
"\n",
"Note here that the program's methods were named *independently* of the communication channels or other processes that invoke them, and instead are named *for the function they perform*. This is a very good practice, and is a mark of well-organized code. Note that although our methods in this case are 1-to-1 mapped to handlers, this doesn't necessarily need to be the case. For example, if you need to do some data processing to convert from the data that you are getting from the communication framework (e.g., a byte-string) to an object that's more convenient to work with in the native language (e.g., a `numpy` array representing an image), then it would be smart to put that in the main file, since the implementation of `MyProgram` shouldn't necessarily be working with the details of how the image is encoded. Your handler code would then look like this:\n",
"\n",
"```python\n",
"...\n",
" new_settings = check_for_settings()\n",
" if new_settings is not None:\n",
" program.set_setting(new_settings)\n",
" img_bytes = check_for_image()\n",
" if img_bytes is not None:\n",
" img_numpy = decode_image(img_bytes)\n",
" program.process_image(img_numpy)\n",
"...\n",
" program.extract_target(img_numpy)\n",
" program.background_task()\n",
" time.sleep(dt)\n",
"```\n",
"\n",
"Note here that the program's methods were named *independently* of the communication channels or other processes that invoke them, and instead are named *for the function they perform*. This is a very good practice, and is a mark of well-organized code. Note that our methods in this case are not 1-to-1 mapped to handlers: the communication processing required to turn an incoming byte string into a numpy array representing an image is handled in the main file since the implementation of `MyProgram` shouldn't necessarily be working with the details of how the image is encoded.\n",
"\n",
"You might also imagine in the future working with someone who specializes in the workings of some algorithm, while you specialize in system integration. You would ask your partner to write a class providing familiar interfaces to their algorithm, while job would be writing the communication \"wrapper\" around it. With this kind of organization, you allow the partner to specialize in their part without needing to know the ins and outs of the system integration system, while you won't need to know all the details of the algorithm, just its top level interface! Separation of application and communication is just the start of sensible code organization for working on teams, as we shall see later in the [system engineering chapter](SystemsEngineering.ipynb#software-organization).\n",
"\n",
"\n",
Expand Down Expand Up @@ -769,11 +764,14 @@
" program.send_another_input(data)\n",
"\n",
"polling_thread = threading.Thread(polling_main,args=(program,lock))\n",
"blocking_thread = threading.Thread(blocking_main,args=(program,lock))\n",
"\n",
"polling_thread.run()\n",
"blocking_thread.run()\n",
"```"
"polling_thread.run() # Start a child thread to execute the polling loop\n",
"blocking_main() # Run the main program in the continuation of the parent thread\n",
"```\n",
"\n",
"This architecture allows for the interweaving of the two loops, and can effectively overlap the latency incurred by polling for data.\n",
"\n",
"NOTE: In many implementations of Python (CPython, PyPy), the [Global Interpreter Lock (GIL)](https://wiki.python.org/moin/GlobalInterpreterLock) prevents multiple threads from executing simultaneously, which can create bottlenecks in compute-bound workloads. I/O operations take place outside the GIL, so their latency can be overlapped using Python threads. To achieve truely concurrent execution across several CPU cores, a separate [process](https://docs.python.org/3/library/multiprocessing.html) (rather than thread) must be spawned. This incurs much more startup and communication overhead than using threads."
]
},
{
Expand Down Expand Up @@ -1150,7 +1148,7 @@
"\n",
"In the prior section, we assumed there were some \"calls\" available that your process could use to communicate with another process. The umbrella term for this process is *inter-process communication* (IPC), and there is a massive diversity of protocols and paradigms that implement IPC in robotics. A good communication package like ROS will abstract a lot of the details of IPC away so you can focus on writing programs. However, you will find that you won't be able to escape bugs, issues, and performance drops without a deeper understanding of an IPC stack. Also, when you are rushing to make a deadline, you don't have the luxury of waiting for someone else to provide ROS bindings to whatever device or program that you want to use. You may also be the programmer responsible for providing those bindings. If any of these cases sound familiar, read on! This section gives a rapid tour of the basic topics that you'd study in a computer science networking class, to give you enough understanding to master IPC.\n",
"\n",
"We'll focus on the programming side of communication, because this is what concerns most system integrators, and we assume that you'll purchase whatever networking equipment or cables will be necessary to connect the hardware pieces of your system. Now, there are several physical mechanisms over which a program on your computer can communicate with programs running on the same computer, running on devices attached to your computer, or running on devices over the internet. The most common mechanisms are: \n",
"We'll focus on the programming side of communication, because this is what concerns most system integrators, and we assume that you'll purchase whatever networking equipment or cables will be necessary to connect the hardware pieces of your system. Now, there are several physical mechanisms over which a process on your computer can communicate with processes running on the same computer, running on devices attached to your computer, or running on devices over the Internet. The most common mechanisms are: \n",
"\n",
"- *APIs*: a program imports another program as a library and directly calls its functions.\n",
"- *Shared memory*: two nodes on the same machine that have access to the same RAM.\n",
Expand Down Expand Up @@ -1465,7 +1463,7 @@
"\n",
"Although in this example, every message sent from the client was received exactly as a single \"chunk\" on the server, this will not always be the case. Depending on the timing of messages and the network implementation, the network read queue may group together the data from multiple send calls, so that the server may receive multiple messages together (e.g., `horsealligator`) or partial messages (e.g., `allig`). This is a common occurrence in serial port protocols, but this is unlikely to happen with small messages sent slowly across a TCP/IP or UDP network, because each message will typically be transmitted in a single *packet* along the network. However, it will occur with large messages sent rapidly. To solve this problem and guarantee that the reader processes full messages, a protocol will need to use some [framing](#framing) strategy. Low-level message queuing libraries, like ZeroMQ, will often provide you with framing functionality while also providing socket-like granularity.\n",
"\n",
"This example should also give you a sense that using raw socket streams to connect to multiple nodes is a rather complex procedure. Here, the `accept` call and each `recv` call were blocking, and the server has to create a new thread to allow for that blocking. If the server also had to send messages to other nodes, it would need to create a thread to process those messages, or take care to configure the server and client sockets to work in non-blocking mode. For this reason, most roboticists use pre-existing communication libraries when available, and resort to low-level socket code only when absolutely necessary. "
"This example should also give you a sense that using raw socket streams to connect to multiple nodes is a rather complex procedure. Here, the `accept` call and each `recv` call were blocking, and the server has to create a new thread to allow for that blocking. If the server also had to send messages to other nodes, it would need to create a thread to process those messages, or take care to configure the server and client sockets to work in non-blocking mode. Python's [`socketserver`](https://docs.python.org/3/library/socketserver.html) library provides some default implementations of such servers but, in practice, most roboticists use pre-existing communication libraries when available, and resort to low-level socket code only when absolutely necessary. "
]
},
{
Expand Down