Tim Cox Raspberry Pi For Python Programmers Cookbook
User Manual: Tim-Cox-Raspberry-Pi-for-Python-Programmers-Cookbook
Open the PDF directly: View PDF .
Page Count: 494
Download | |
Open PDF In Browser | View PDF |
Raspberry Pi for Python Programmers Cookbook Second Edition Over 60 recipes that harness the power of the Raspberry Pi together with Python programming and create enthralling and captivating projects Tim Cox BIRMINGHAM - MUMBAI Raspberry Pi for Python Programmers Cookbook Second Edition Copyright © 2016 Packt Publishing All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, without the prior written permission of the publisher, except in the case of brief quotations embedded in critical articles or reviews. Every effort has been made in the preparation of this book to ensure the accuracy of the information presented. However, the information contained in this book is sold without warranty, either express or implied. Neither the author, nor Packt Publishing, and its dealers and distributors will be held liable for any damages caused or alleged to be caused directly or indirectly by this book. Packt Publishing has endeavored to provide trademark information about all of the companies and products mentioned in this book by the appropriate use of capitals. However, Packt Publishing cannot guarantee the accuracy of this information. First published: April 2014 Second edition: September 2016 Production reference: 1270916 Published by Packt Publishing Ltd. Livery Place 35 Livery Street Birmingham B3 2PB, UK. ISBN 978-1-78528-832-6 www.packtpub.com Table of Contents Preface Chapter 1: Getting Started with a Raspberry Pi Computer v 1 Introduction Connecting the Raspberry Pi Using NOOBS to set up your Raspberry Pi SD card Networking and connecting your Raspberry Pi to the Internet via the LAN connector Using built-in Wi-Fi and Bluetooth on the Raspberry Pi Configuring your network manually Networking directly to a laptop or computer Networking and connecting your Raspberry Pi to the Internet via a USB Wi-Fi dongle Connecting to the Internet through a proxy server Connecting remotely to the Raspberry Pi over the network using VNC Connecting remotely to the Raspberry Pi over the network using SSH (and X11 Forwarding) Sharing the home folder of the Raspberry Pi with SMB Keeping the Raspberry Pi up to date 1 6 10 23 24 27 31 40 45 48 50 55 57 Chapter 2: Starting with Python Strings, Files, and Menus 61 Chapter 3: Using Python for Automation and Productivity 85 Introduction Working with text and strings Using files and handling errors Creating a boot-up menu Creating a self-defining menu 61 62 72 76 80 Introduction Using Tkinter to create graphical user interfaces 85 86 i Table of Contents Creating a graphical application – Start menu Displaying photo information in an application Organizing your photos automatically 91 96 105 Chapter 4: Creating Games and Graphics 111 Chapter 5: Creating 3D Graphics 137 Chapter 6: Using Python to Drive Hardware 173 Chapter 7: Sense and Display Real-World Data 225 Chapter 8: Creating Projects with the Raspberry Pi Camera Module 283 Introduction Using IDLE3 to debug your programs Drawing lines using a mouse on Tkinter Canvas Creating a bat and ball game Creating an overhead scrolling game Introduction Starting with 3D coordinates and vertices Creating and importing 3D models Creating a 3D world to roam in Building 3D maps and mazes Introduction Controlling an LED Responding to a button A controlled shutdown button The GPIO keypad input Multiplexed color LEDs Writing messages using Persistence of Vision Introduction Using devices with the I2C bus Reading analog data using an analog-to-digital converter Logging and plotting data Extending the Raspberry Pi GPIO with an I/O expander Capturing data in an SQLite database Viewing data from your own webserver Sensing and sending data to online services Introduction Getting started with the Raspberry Pi camera module Using the camera with Python Generating a time-lapse video Creating a stop frame animation Making a QR code reader ii 111 112 116 118 126 137 138 147 153 158 173 178 184 190 197 203 214 225 226 236 243 252 259 267 275 283 284 288 296 306 316 Table of Contents Discover and experiment with OpenCV Color detection with OpenCV Performing motion tracking with OpenCV 322 328 338 Chapter 9: Building Robots 349 Chapter 10: Interfacing with Technology 421 Appendix: Hardware and Software List Index 481 483 Introduction Building a Rover-Pi robot with forward driving motors Using advanced motor control Building a six-legged Pi-Bug robot Controlling servos directly with Servoblaster Using an Infra-Red Remote Control with your Raspberry Pi Avoiding objects and obstacles Getting a sense of direction Introduction Automating your home with remote sockets Using SPI to control an LED matrix Communicating using a serial interface Controlling the Raspberry Pi over Bluetooth Controlling USB devices 349 350 369 376 387 395 402 410 421 422 435 449 464 469 iii Preface Since the release of the Raspberry Pi computer in February 2012, millions of people have been introduced to a new way of computing. Modern home computers, tablets, and phones are typically focused on providing content to the user to consume, either as a passive viewer or through basic interaction via games and activities. However, the Raspberry Pi turns this concept on its head. The idea is that the user provides the input and the imagination, and the Raspberry Pi becomes an extension of their creativity. The Raspberry Pi provides a simple, low-cost platform that you can use to experiment with and play with your own ideas. It won't feed you information; it will let you discover it firsthand. This book takes everything I have found exciting and interesting with the Raspberry Pi and puts it in an easy-to-follow format. I hope that people will read this book and start their own Raspberry Pi journey; it has so much to offer, and the book is aimed squarely at showing off what you can achieve with it. Like any good cookbook, the pages should be worn and used, and it should be something that is always being pulled off the shelf to refer to. I hope it will become your own, personal, go-to reference. What this book covers Chapter 1, Getting Started with a Raspberry Pi Computer, introduces the Raspberry Pi and explores the various ways that it can be set up and used, including how it can be used on a network and connected to remotely with another computer. Chapter 2, Starting with Python Strings, Files, and Menus, guides us on how to take our first steps using Python 3, start with the basics, manipulate text, use files, and create menus to run our programs. Chapter 3, Using Python for Automation and Productivity, explains the use of graphical user interfaces to create our own applications and utilities. v Preface Chapter 4, Creating Games and Graphics, explains how to create a drawing application and graphical games using the Tkinter Canvas. Chapter 5, Creating 3D Graphics, discusses how we can use the hidden power of the Raspberry Pi's graphical processing unit to learn about 3D graphics and landscapes and produce our very own 3D maze for exploration. Chapter 6, Using Python to Drive Hardware, establishes the fact that to experience the Raspberry Pi at its best, we really have to use it with our own electronics. It discusses how to create circuits with LEDs and switches, and use them to indicate the system status and provide control. Finally, it shows us how to create our own game controller, light display and a persistence of vision text display. Chapter 7, Sense and Display Real-World Data, explains the use of an analog-to-digital convertor to provide sensor readings to the Raspberry Pi. We discover how to store and graph the data in real time, as well as display it on an LCD text display. Next we record the data in a SQL database and display it in our own webserver. Finally, we transfer the data to the Internet, which will allow us to view and share the captured data anywhere in the world. Chapter 8, Creating Projects with the Raspberry Pi Camera Module, teaches us how to use the Raspberry Pi camera module, creating our own applications to produce time-lapse videos, stop-frame animations, and a bedtime book reader controlled with QR codes. Additionally we make use of the immensely powerful image processing library OpenCV to perform color recognition and object (or in this case, a tortoise) tracking. Chapter 9, Building Robots, takes you through building two different types of robots (a Rover- Pi and a Pi-Bug), plus driving a servo-based robot arm. We look at motor and servo control methods, using sensors, and adding a compass sensor for navigation. Chapter 10, Interfacing with Technology, teaches us how to use the Raspberry Pi to trigger remote mains sockets, with which we can control household appliances. We learn how to communicate with the Raspberry Pi over a serial interface and use a smartphone to control everything using Bluetooth. Finally, we look at creating our own applications to control USB devices. Appendix, Hardware and Software List, provides us with the full list of the hardware components and modules used in the book, along with suitable places to purchase them from. A full list of the software used is also provided, along with links to documentation. What you need for this book This book focuses on using the Raspberry Pi with Python 3; therefore, a basic Raspberry Pi setup is required. Chapters 1 to 5 of this book make use of the Raspberry Pi only; no additional hardware is required beyond a standard setup. vi Preface The standard setup will consist of a Raspberry Pi (Model A or Model B, Version 1, 2 or 3); an SD card installed with Raspbian; a suitable micro USB power supply; and an HDMI-compatible screen, keyboard, and mouse. You will also be required to download and install various software packages; therefore, the Raspberry Pi should have a working internet connection. Chapter 1, Getting Started with a Raspberry Pi Computer, also describes how to use the screen/keyboard/mouse of a laptop or another computer to access the Raspberry Pi (you just need a network cable and power). Chapter 6, Using Python to Drive Hardware, and Chapter 7, Sense and Display Real-World Data, show how electronic components can be connected to the Raspberry Pi's interfaces. These components will be needed in order to complete these chapters. Chapter 8, Creating Projects with the Raspberry Pi Camera Module, requires the Raspberry Pi camera module for each of the projects (although a compatible USB webcam could be substituted by adjusting the code). Chapter 9, Building Robots, uses a range of hardware and electronics to build your own robots. You can either use your own parts or a suitable kit for this. Chapter 10, Interfacing with Technology, shows how additional hardware can be connected to the interfaces of the Raspberry Pi using various modules and kits. A full list of the hardware used (and the possible places to purchase it from) has been provided in the Appendix, Hardware and Software List. Who this book is for This book is intended for anyone who wants to make the most of the Raspberry Pi experience. The book gradually introduces Python, starting with the basics and moving towards more advanced topics, such as using 3D graphics and interfacing with hardware. Although you do not need to be familiar with Python, the Raspberry Pi, or electronics, this book touches on a wide range of topics. Ideally, you should give each chapter a try, see what you enjoy, and use that as a starting point to discover and learn more. Each example in the book consists of full setup instructions, complete code listings, and a walk-through of what you did and why. This will allow you to get results quickly, and most importantly, understand how you achieved them. All the examples are written using Python 3, with clear and detailed explanations of how everything works so that you can adapt and use all the information in your own projects. As you progress through the book, it will explain how to structure and develop your code efficiently, building on the various techniques that can be applied as you progress. By the end, you will have a toolset of skills that you can apply to whatever your imagination inspires you to do. vii Preface Safety and using electronics This book encourages you to experiment and connect your own circuits to the general-purpose input/output Raspberry Pi GPIO pins. This is an excellent way to learn about electronics and software at the same time. However, it is important to remember that the GPIO pins are unprotected, and if wired incorrectly, can easily be damaged or even cause the Raspberry Pi to stop working altogether. Therefore, care should be taken to correctly follow the instructions and wiring diagrams and check everything carefully before switching the Raspberry Pi on. All the circuits, modules, and components described in this book are intended as demonstration examples only. They have not been tested for extended use and should not be left unattended or should not be used in safety-critical applications without adequate safeguards in place. Remember that all electronics must undergo rigorous safety testing to ensure that in the event of failure, there will be no risk of harm to people or property. You should never attempt to modify or alter devices that are connected to mains electricity without proper training, and you must never directly connect any homemade devices to the mains supply. Sections In this book, you will find several headings that appear frequently (Getting ready, How to do it, How it works, There's more, and See also). To give clear instructions on how to complete a recipe, we use these sections as follows: Getting ready This section tells you what to expect in the recipe, and describes how to set up any software or any preliminary settings required for the recipe. How to do it… This section contains the steps required to follow the recipe. How it works… This section usually consists of a detailed explanation of what happened in the previous section. viii Preface There's more… This section consists of additional information about the recipe in order to make the reader more knowledgeable about the recipe. See also This section provides helpful links to other useful information for the recipe. Conventions In this book, you will find a number of styles of text that distinguish between different kinds of information. Here are some examples of these styles, and an explanation of their meaning. Code words in text, database table names, folder names, filenames, file extensions, pathnames, dummy URLs, user input, and Twitter handles are shown as follows: "On a freshly formatted or new SD card, copy the contents of the NOOBS_vX.zip file." A block of code is set as follows: network={ ssid="theSSID" key_mgmt=NONE } Any command-line input or output is written as follows: sudo mount –t vfat /dev/mmcblk0p1 ~/recovery New terms and important words are shown in bold. Words that you see on the screen, in menus or dialog boxes for example, appear in the text like this: "For OS X or Linux, click on Terminal to open a connection to the Raspberry Pi." Warnings or important notes appear in a box like this. Tips and tricks appear like this. ix Preface Reader feedback Feedback from our readers is always welcome. Let us know what you think about this book— what you liked or disliked. Reader feedback is important for us as it helps us develop titles that you will really get the most out of. To send us general feedback, simply e-mail feedback@packtpub.com, and mention the book's title in the subject of your message. If there is a topic that you have expertise in and you are interested in either writing or contributing to a book, see our author guide at www.packtpub.com/authors. Customer support Now that you are the proud owner of a Packt book, we have a number of things to help you to get the most from your purchase. Downloading the example code You can download the example code files for this book from your account at http:// www.packtpub.com. If you purchased this book elsewhere, you can visit http://www. packtpub.com/support and register to have the files e-mailed directly to you. You can download the code files by following these steps: 1. Log in or register to our website using your e-mail address and password. 2. Hover the mouse pointer on the SUPPORT tab at the top. 3. Click on Code Downloads & Errata. 4. Enter the name of the book in the Search box. 5. Select the book for which you're looking to download the code files. 6. Choose from the drop-down menu where you purchased this book from. 7. Click on Code Download. You can also download the code files by clicking on the Code Files button on the book's webpage at the Packt Publishing website. This page can be accessed by entering the book's name in the Search box. Please note that you need to be logged in to your Packt account. x Preface Once the file is downloaded, please make sure that you unzip or extract the folder using the latest version of: ff WinRAR / 7-Zip for Windows ff Zipeg / iZip / UnRarX for Mac ff 7-Zip / PeaZip for Linux The code bundle for the book is also hosted on GitHub at https://github.com/ PacktPublishing/Raspberry-Pi-for-Python-Programmers-Cookbook-SecondEdition. We also have other code bundles from our rich catalog of books and videos available at https://github.com/PacktPublishing/. Check them out! Errata Although we have taken every care to ensure the accuracy of our content, mistakes do happen. If you find a mistake in one of our books—maybe a mistake in the text or the code—we would be grateful if you could report this to us. By doing so, you can save other readers from frustration and help us improve subsequent versions of this book. If you find any errata, please report them by visiting http://www.packtpub.com/submit-errata, selecting your book, clicking on the Errata Submission Form link, and entering the details of your errata. Once your errata are verified, your submission will be accepted and the errata will be uploaded to our website or added to any list of existing errata under the Errata section of that title. To view the previously submitted errata, go to https://www.packtpub.com/books/ content/support and enter the name of the book in the search field. The required information will appear under the Errata section. Piracy Piracy of copyrighted material on the Internet is an ongoing problem across all media. At Packt, we take the protection of our copyright and licenses very seriously. If you come across any illegal copies of our works in any form on the Internet, please provide us with the location address or website name immediately so that we can pursue a remedy. Please contact us at copyright@packtpub.com with a link to the suspected pirated material. We appreciate your help in protecting our authors and our ability to bring you valuable content. Questions If you have a problem with any aspect of this book, you can contact us at questions@ packtpub.com, and we will do our best to address the problem. xi 1 Getting Started with a Raspberry Pi Computer In this chapter, we will cover the following recipes: ff Connecting the Raspberry Pi ff Using NOOBS to set up your Raspberry Pi SD card ff Networking and connecting your Raspberry Pi to the Internet via the LAN connector ff Using built-in Wi-Fi and Bluetooth on the Raspberry Pi ff Configuring your network manually ff Networking directly to a laptop or computer ff Networking and connecting your Raspberry Pi to the Internet via a USB Wi-Fi dongle ff Connecting to the Internet through a proxy server ff Connecting remotely to the Raspberry Pi over the network using VNC ff Connecting remotely to the Raspberry Pi over the network using SSH (and X11 Forwarding) ff Sharing the home folder of the Raspberry Pi with SMB ff Keeping the Raspberry Pi up to date Introduction This chapter introduces the Raspberry Pi and the process to set it up for the first time. We will connect the Raspberry Pi to a suitable display, power, and peripherals. We will install an operating system on an SD card. This is required for the system to boot. Next, we will ensure that we can connect successfully to the Internet through a local network. 1 Getting Started with a Raspberry Pi Computer Finally, we will make use of the network to provide ways to remotely connect to and/or control the Raspberry Pi from other computers and devices, as well as to ensure that the system is kept up to date. Once you have completed the steps within this chapter, your Raspberry Pi will be ready for you to use for programming. If you already have your Raspberry Pi set up and running, ensure that you take a look through the following sections as there are many helpful tips. Introducing the Raspberry Pi The Raspberry Pi is a single-board computer created by the Raspberry Pi Foundation, a charity formed with the primary purpose of reintroducing low-level computer skills to children in the UK. The aim was to rekindle the microcomputer revolution of the 1980s, which produced a whole generation of skilled programmers. Even before the computer was released at the end of February 2012, it was clear that the Raspberry Pi had gained a huge following worldwide and, at the time of writing this book, has sold over 10 million units. The following image shows several different Raspberry Pi Models: Raspberry Pi Model 3B, Model A+, and Pi Zero What is with the name? The name, Raspberry Pi, was the combination of the desire to create an alternative fruit-based computer (such as Apple, BlackBerry, and Apricot) and a nod to the original concept of a simple computer that can be programmed using Python (shortened to Pi). 2 Chapter 1 In this book, we will take this little computer, find out how to set it up, and then explore its capabilities chapter by chapter using the Python programming language. Why Python? It is often asked, "Why has Python been selected as the language to use on the Raspberry Pi?" The fact is that Python is just one of the many programming languages that can be used on the Raspberry Pi. There are many programming languages that you can choose, from high-level graphical block programming, such as Scratch, to traditional C, right down to BASIC, and even raw Machine Code Assembler. A good programmer often has to be code multilingual to be able to play to the strengths and weaknesses of each language in order to best meet the needs of their desired application. It is useful to understand how different languages (and programming techniques) try to overcome the challenge of converting "what you want" into "what you get" as this is what you are trying to do as well while you program. Python has been selected as a good place to start when learning about programming, by providing a rich set of coding tools while still allowing simple programs to be written without fuss. This allows beginners to gradually be introduced to the concepts and methods on which modern programming languages are based without requiring them to know it all from the start. It is very modular with lots of additional libraries that can be imported to quickly extend the functionality. You will find that over time, this encourages you to do the same, and you will want to create your own modules that you can plug into your own programs, thus taking your first steps into structured programming. Like all programming languages, Python isn't perfect; things such as adding a space at the start of a line will often break your code (indents matter a lot in Python; they define how blocks of code are grouped together). Generally, Python is slow; since it is interpreted, it takes time to create a module while it is running the program. This can be a problem if you need to respond to time critical events. However, you can precompile Python or use modules written in other languages to overcome this. It hides the details; this is both an advantage and disadvantage. It is excellent for beginners but can be difficult when you have to second-guess aspects such as data-types. However, this in turn forces you to consider all the possibilities, which can be a good thing. Python 2 and Python 3 A massive source of confusion for beginners is that there are two versions of Python on the Raspberry Pi (Version 2.7 and Version 3.4), which are not compatible with one another, so code written for Python 2.7 may not run with Python 3.4 (and vice versa). The Python Software Foundation is continuously working to improve and move forward with the language, which sometimes means they have to sacrifice backward compatibility in order to embrace new improvements (and importantly, remove redundant and legacy ways of doing things). 3 Getting Started with a Raspberry Pi Computer Supporting both Python 2 or Python 3 There are many tools that will ease the transition from Python 2 to Python 3, including converters such as 2to3, which will parse and update your code to use Python 3 methods. This process is not perfect, and in some cases, you'll need to manually rewrite sections and fully retest everything. You can write the code and libraries that will support both. The import __future__ statement allows you to import the friendly methods of Python 3 and run them using Python 2.7. Which version of Python should you use? Essentially, the selection of which version to use will depend on what you intend to do. For instance, you may require Python 2.7 libraries, which are not yet available for Python 3.4. Python 3 has been available since 2008, so these tend to be older or larger libraries that have not been translated. In many cases, there are new alternatives to legacy libraries; however, their support can vary. In this book, we have used Python 3.4, which is also compatible with Python 3.3 and 3.2. The Raspberry Pi family – a brief history of Pi Since its release, the Raspberry Pi has come in various iterations, featuring both small and large updates and improvements to the original Raspberry Pi Model B unit. Although it can be confusing at first, there are three basic types of Raspberry Pi available (and one special model). The main flagship model is called Model B. This has all the connections and features, as well as the maximum RAM and the latest processor. Over the years, there have been several versions, most notably Model B (which had 256 MB and then 512 MB RAM) and then Model B+ (which increased the 26-pin GPIO to 40 pins, switched to using a micro SD card slot, and had four USB ports instead of two). These original models all used the Broadcom BCM2835 SOC (short for System On Chip), consisting of a single core 700 MHz ARM11 and VideoCore IV GPU (short for Graphical Processing Unit). The release of the Raspberry Pi 2 Model B (also referred to as 2B) in 2015 introduced a new Broadcom BCM2836 SOC, providing a quad-core 32-bit ARM Cortex A7 1.2 GHz processor and GPU, with 1 GB of RAM. The improved SOC added support for Ubuntu and Windows 10 IoT. Finally we had the latest Raspberry Pi 3 Model B, using another new Broadcom BCM2837 SOC, which provides a quad-core 64-bit ARM Cortex-A53 and GPU, alongside adding on-board Wi-Fi and Bluetooth. 4 Chapter 1 Model A has always been targeted as a cut-down version. While having the same SOC as Model B, there are limited connections consisting of a single USB port and no wired network (LAN). Model A+ again added more GPIO pins and a micro SD slot. However, the RAM was later upgraded to 512 MB of RAM and again only a single USB port/no LAN. The Broadcom BCM2835 SOC on Model A has not been updated so far (so is still a single core ARM11); however, a Model 3A (most likely using the BCM2837) is expected 2016/2017. The Pi Zero is an ultra-compact version of the Raspberry Pi intended for embedded applications where cost and space are a premium. It has the same 40-pin GPIO and micro SD card slot as the other models, but lacks the on-board display (CSI and DSI) connection. It does still have HDMI (via a mini-HDMI) and a single micro USB OTG (on-the-go) connection. Although not present in the first revision of the Pi Zero, the most recent model also includes a CSI connection for the on-board camera. The Pi Zero was famously released in 2015 by being given away with the Raspberry Pi foundations magazine The MagPi, giving the magazine the kudos of being the first magazine to give away a computer on its cover! This did make me rather proud since (as you may have read in my biography at the start of this book) I was one of the founders of the magazine. The special model is known as the compute module. This takes the form of a 200-pin SO-DIMM card. It is intended for industrial use or within commercial products, where all the external interfaces would be provided by a host/motherboard, into which the module would be inserted. Example products include the Slice media player (http://fiveninjas.com) and the Otto camera. The current module uses the BCM2835, although an updated compute module (CM3) is expected in 2016. The Raspberry Pi Wikipedia page provides a full list of the all different variants and their specifications: https://en.wikipedia.org/wiki/Raspberry_Pi#Specifications Which Pi to choose? All sections of this book are compatible will all current versions of the Raspberry Pi, but Model 3B is recommended as the best model to start with. This offers the best performance (particularly useful for the GPU examples in Chapter 5, Creating 3D Graphics, and the OpenCV examples used in Chapter 8, Creating Projects with the Raspberry Pi Camera Module), lots of connections, and built-in Wi-Fi, which can be very convenient. The Pi Zero is recommended for projects where you want low power usage or reduced weight/size but do not need the full processing power of Model 3B. However, due to its ultra-low cost, the Pi Zero is ideal for deploying a completed project after you have developed it. 5 Getting Started with a Raspberry Pi Computer Connecting the Raspberry Pi There are many ways to wire up the Raspberry Pi and use the various interfaces to view and control content. For typical use, most users will require power, display (with audio), and a method of input such as keyboard and mouse. To access the Internet, refer to the Networking and connecting your Raspberry Pi to the Internet via the LAN connector or Using built-in Wi-Fi and Bluetooth on the Raspberry Pi recipes. Getting ready Before you can use your Raspberry Pi, you will need an SD card with an operating system installed or with the New Out Of Box System (NOOBS) on it, as discussed in the Using NOOBS to set up your Raspberry Pi SD card recipe. The following section will detail the types of devices you can connect to the Raspberry Pi and, importantly, how and where to plug them in. As you will discover later, once you have your Raspberry Pi set up, you may decide to connect remotely and use it through a network link, in which case you only need power and a network connection. Refer to the following sections: Connecting remotely to the Raspberry Pi over the network using VNC and Connecting remotely to the Raspberry Pi over the network using SSH (and X11 Forwarding). How to do it… The layout of the Raspberry Pi is shown in the following figure: The Raspberry Pi connection layout (Model 3 B, Model A+, and Pi Zero) 6 Chapter 1 The description of the preceding figure is as follows: ff Display: The Raspberry Pi supports the following three main display connections; if both HDMI and Composite video are connected, it will default to HDMI only. HDMI For best results, use a TV or monitor that has an HDMI connection, thus allowing the best resolution display (1080p) and also digital audio output. If your display has a DVI connection, you may be able to use an adapter to connect through the HDMI. There are several types of DVI connections; some support analogue (DVI-A), some digital (DVI-D), and some both (DVI-I). The Raspberry Pi is only able to provide a digital signal through the HDMI, so an HDMI-to-DVI-D adapter is recommended (shown with a tick mark in the following screenshot). This lacks the four extra analogue pins (shown with a cross mark in the following screenshot), thus allowing it to fit into both DVI-D and DVI-I type sockets: HDMI-to-DVI connection (DVI-D adaptor) If you wish to use an older monitor (with a VGA connection), an additional HDMI-to-VGA converter is required. The Raspberry Pi also supports a rudimentary VGA adaptor (VGA Gert666 Adaptor), which is driven directly off of the GPIO pins. However, this does use up all but 4 pins of the 40-pin header (older 26-pin models will not support the VGA output). Analogue An alternative display method is to use the analogue composite video connection (via the phono socket); this can also be attached to an S-Video or European SCART adapter. However, the analogue video output has a maximum resolution of 640 x 480 pixels, so it is not ideal for general use. 3.5mm phono analogue connections 7 Getting Started with a Raspberry Pi Computer When using the RCA connection or a DVI input, audio has to be provided separately by the analogue audio connection. To simplify the manufacturing process (by avoiding through-hole components), the Pi Zero does not have analogue audio or an RCA socket for analogue video (although they can be added with some modifications). Direct Display DSI A touch display produced by the Raspberry Pi Foundation will connect directly into the DSI socket. This can be connected and used at the same time as the HDMI or analogue video output to create a dual display setup. ff Stereo Analogue Audio (all except Pi Zero): This provides an analogue audio output for headphones or amplified speakers. The audio can be switched via the Raspberry Pi configuration tool on the desktop between analog (Stereo Socket) and digital (HDMI), or via the command line using amixer or alsamixer. To find out more information about a particular command in the terminal, you can use the following man command before the terminal reads the manual (most commands should have one): man amixer Some commands also support the --help option for more concise help, shown as follows: amixer --help ff Network (excluding models A and Pi Zero): The network connection is discussed in the Networking and connecting your Raspberry Pi to the Internet via the LAN connector recipe later in this chapter. If we use the Model A Raspberry Pi, it is possible to add a USB network adapter to add wired or even wireless networking (refer to the Networking and connecting your Raspberry Pi to the Internet via a USB Wi-Fi dongle recipe). ff Onboard Wi-Fi and Bluetooth (Model 3 B only): ff The Model 3 B has built-in 802.11n Wi-Fi and Bluetooth 4.1; see the Using the built-in Wi-Fi and Bluetooth on the Raspberry Pi recipe. ff USB (x1 Model A/Zero, x2 Model 1 B, x4 Model 2 B and 3 B)—using a keyboard and mouse: The Raspberry Pi should work with most USB keyboards and mice available. You can also use wireless mice and keyboards, which use RF Dongles. However, additional configuration is required for items that use the Bluetooth dongles. If there is a lack of power supplied by your power supply or the devices are drawing too much current, you may experience the keyboard keys appearing to stick, and in severe cases, corruption of the SD card. 8 Chapter 1 USB power can be more of an issue with the early Model B revision 1 boards that were available prior to October 2012. They included additional Polyfuses on the USB output and tripped if an excess of 140 mA was drawn. The Polyfuses can take several hours or days to recover completely, thus causing unpredictable behavior to remain even when the power is improved. You can identify a revision 1 board as it lacks the four mounting holes that are present the later models. Debian Linux (upon which Raspbian is based) supports many common USB devices, such as flash storage drives, hard disk drives (external power may be required), cameras, printers, Bluetooth, and Wi-Fi adapters. Some devices will be detected automatically while others will require drivers to be installed. ff Micro USB Power: The Raspberry Pi requires a 5V power supply that can comfortably supply at least 1000 mA (1,500 mA or more is recommended, particularly with the more power-hungry Model 2 and 3) with a micro USB connection. It is possible to power the unit using portable battery packs, such as the ones suitable for powering or recharging tablets. Again, ensure that they can supply 5V at 1000 mA or over. You should aim to make all other connections to the Raspberry Pi before connecting the power. However, USB devices, audio, and network may be connected and removed while it is running without problems. There's more… In addition to the standard primary connections you would expect to see on a computer, the Raspberry Pi also has a number of other connections. Secondary hardware connections Each of the following connections provides additional interfaces for the Raspberry Pi: ff 20 x 2 GPIO pin header (Model A+, B+, 2 B, 3 B, and Pi Zero): This is the main 40-pin GPIO header of the Raspberry Pi used for interfacing directly with hardware components. We use this connection in Chapters 6, 7, 9, and 10. The recipes in this book are also compatible with older models of the Raspberry Pi that have a 13 x 2 GPIO pin header. ff P5 8 x 2 GPIO pin header (Model 1 B revision 2.0 only): We do not use this in the book. ff Reset connection: This is present on later models (no pins fitted). A reset is triggered when Pin 1 (reset) and Pin 2 (GND) are connected together. We use this in the A controlled shutdown button recipe in Chapter 6, Using Python to Drive Hardware. 9 Getting Started with a Raspberry Pi Computer ff GPU/LAN JTAG: The Joint Test Action Group (JTAG) is a programming and debugging interface used to configure and test processors. These are present on newer models as surface pads. A specialist JTAG device is required to use this interface. We do not use this in the book. ff Direct camera CSI: This connection supports the Raspberry Pi Camera module (as used in Chapter 8, Creating Projects with the Raspberry Pi Camera Module). Note that the Pi Zero has a smaller CSI connector than the other models, so it requires a different ribbon connector. ff Direct display DSI: This connection supports a directly connected display, a 7-inch 800 x 600 capacitive touch screen. Using NOOBS to set up your Raspberry Pi SD card The Raspberry Pi requires the operating system to be loaded onto an SD card before it starts up. The easiest way to set up the SD card is to use NOOBS; you may find that you can buy an SD card with NOOBS already loaded on it. NOOBS provides an initial start menu that provides options to install several of the available operating systems onto your SD card. Getting ready Since NOOBS creates a RECOVERY partition to keep the original installation images, an 8-GB SD card or larger is recommended. You will also need an SD card reader (experience has shown that some built-in card readers can cause issues, so an external USB type reader is recommended). If you are using an SD card that you have used previously, you may need to reformat it to remove any previous partitions and data. NOOBS expects the SD card to consist of a single FAT32 partition. If using Windows or Mac OS X, you can use the SD association's formatter, as shown in the following screenshot (available at https://www.sdcard.org/downloads/ formatter_4/): 10 Chapter 1 Get rid of any partitions on the SD card using SD formatter From the Option Setting dialog box, set Format Size Adjustment. This will remove all the SD card partitions that were created previously. If using Linux, you can use gparted to clear any previous partitions and reformat it as a FAT32 partition. The full NOOBS package (typically just over 1 GB) contains the Raspbian, the most popular Raspberry Pi operating system image built in. A lite version of NOOBS is also available that has no preloaded operating systems (although a smaller initial download of 20 MB and a network connection on the Raspberry Pi are required to directly download the operating system you intend to use). NOOBS is available at http://www.raspberrypi.org/downloads, with the documentation available at https://github.com/raspberrypi/noobs. 11 Getting Started with a Raspberry Pi Computer How to do it… By performing the following steps, we will prepare the SD card to run NOOBS. This will then allow us to select and install the operating system we want to use: 1. Get your SD card ready. 2. On a freshly formatted or new SD card, copy the contents of the NOOBS_vX.zip file. When it has finished copying, you should end up with something like the following screenshot of the SD card: NOOBS files extracted onto the SD card The files may vary slightly with different versions of NOOBS, and the icons displayed may be different on your computer. 3. You can now put the card into your Raspberry Pi, connect it to a keyboard and display, and turn the power on. Refer to the Connecting up the Raspberry Pi recipe for details on what you need and how to do this. 12 Chapter 1 By default, NOOBS will display via the HDMI connection. If you have another type of screen (or you don't see anything), you will need to manually select the output type by pressing 1, 2, 3, or 4 according to the following functions: ff Key 1 stands for the Standard HDMI mode (the default mode) ff Key 2 stands for the Safe HDMI mode (alternative HDMI settings if the output has not been detected) ff Key 3 stands for Composite PAL (for connections made via the RCA analogue video connection) ff Key 4 stands for Composite NTSC (again, for connections via the RCA connector) This display setting will also be set for the installed operating system. After a short while, you will see the NOOBS selection screen that lists the available distributions (the offline version only includes Raspbian). There are many more distributions that are available, but only the selected ones are available directly through the NOOBS system. Click on Raspbian as this is the operating system being used in this book. Press Enter or click on Install OS, and confirm that you wish to overwrite all the data on the card. This will overwrite any distributions previously installed using NOOBS but will not remove the NOOBS system; you can return to it at any time by pressing Shift when you turn the power on. It will take around 20 to 40 minutes to write the data to the card depending on its speed. When it completes and the Image Applied Successfully message appears, click on OK and the Raspberry Pi will start to boot into the Raspberry Pi Desktop. How it works… The purpose of writing the image file to the SD card in this manner is to ensure that the SD card is formatted with the expected filesystem partitions and files required to correctly boot the operating system. When the Raspberry Pi powers up, it loads some special code contained within the GPU's internal memory (commonly referred to as binary blob by the Raspberry Pi Foundation). The binary blob provides the instructions required to read the BOOT Partition on the SD card, which (in the case of a NOOBS install) will load NOOBS from the RECOVERY partition. If at this point Shift is pressed, NOOBS will load the recovery and installation menu. Otherwise, NOOBS will begin loading the OS as specified by the preferences stored in the SETTINGS Partition. 13 Getting Started with a Raspberry Pi Computer When loading the operating system, it will boot via the BOOT partition using the settings defined in config.txt and options in cmdline.txt to finally load to the desktop on the root Partition. Refer to the following diagram: NOOBS creates several partitions on the SD card to allow installation of multiple operating systems and provide recovery NOOBS allows the user to optionally install multiple operating systems on the same card and provides a boot menu to select between them (with an option to set a default value in the event of a time-out period). If you later add, remove, or reinstall an operating system, ensure first that you make a copy of any files, including system settings you wish to keep, as NOOBS may overwrite everything on the SD card. There's more… When you power up the Raspberry Pi for the first time, it will start directly with the desktop. You can now configure the system settings using the Raspberry Pi Configuration program (under the Preferences menu on the Desktop or via the sudo raspi-config command), which will allow you to perform changes to your SD card and set up your general preferences. 14 Chapter 1 Raspberry Pi Configuration program Changing the default user password Ensure that you change the default password for the pi user account once you have logged in, as the default password is well known. This is particularly important if you connect to public networks. You can do this with the passwd command, as shown in the following screenshot: Setting a new password for the pi user This gives greater confidence because if you later connect to another network, only you will be able to access your files and take control of your Raspberry Pi. Ensuring that you shut down safely To avoid any data corruption, you must ensure that you correctly shut down the Raspberry Pi by issuing a shutdown command, as follows: sudo shutdown –h now 15 Getting Started with a Raspberry Pi Computer Or use this one: sudo halt You must wait until this command completes before you remove power from the Raspberry Pi (wait for at least 10 seconds after the SD card access light has stopped flashing). You can also restart the system with the reboot command, as follows: sudo reboot Preparing an SD card manually An alternative to using NOOBS is to manually write the operating system image to the SD card. While this was originally the only way to install the operating system, some users still prefer it. It allows the SD cards to be prepared before they are used in the Raspberry Pi. It can also provide easier access to startup and configuration files, and it leaves more space available for the user (unlike NOOBS, a RECOVERY partition isn't included). The default Raspbian image actually consists of two partitions, BOOT and SYSTEM, which will fit into a 2 GB SD card (4 GB or more is recommended). You need a computer running Windows/Mac OS X/Linux (although it is possible to use another Raspberry Pi to write your card, be prepared for a very long wait). Download the latest version of the operating system you wish to use. For the purpose of this book, it is assumed you are using the latest version of Raspbian available at http://www. raspberrypi.org/downloads. Perform the following steps depending on the type of computer you plan to use to write to the SD card (the .img file you need is sometimes compressed, so before you start, you will need to extract the file). The following steps are for Windows: 1. Ensure that you have downloaded the Raspbian image, as previously detailed, and extracted it to a convenient folder to obtain an .img file. 2. Obtain the Win32DiskImager.exe file available at http://www.sourceforge. net/projects/win32diskimager. 3. Run Win32DiskImager.exe from your downloaded location. 4. Click on the folder icon and navigate to the location of the .img file and click on Save. 5. If you haven't already done so, insert your SD card into your card reader and plug it into your computer. 6. Select the Device drive letter that corresponds to your SD card from the small drop-down box. Double-check that this is the correct device (as the program will overwrite whatever is on the device when you write the image). 16 Chapter 1 The drive letter may not be listed until you select a source image file. 7. Finally, click on the Write button and wait for the program to write the image to the SD card, as shown in the following screenshot: Manually write operating system images to the SD card using Disk Imager Once completed, you can exit the program. Your SD card is ready! The following steps should work for the most common Linux distributions, such as Ubuntu and Debian: 1. Using your preferred web browser, download the Raspbian image and save it in a suitable place. 2. Extract the file from the file manager or locate the folder in the terminal and unzip the .img file with the following command: unzip filename.zip 3. If you haven't already done so, insert your SD card into your card reader and plug it into your computer. 4. Use the df –h command and identify the sdX identifier for the SD card. Each partition will be displayed as sdX1, sdX2, and so on, where X will be a, b, c, d, and so on for the device ID. 5. Ensure that all the partitions on the SD card are unmounted using the umount /dev/sdXn command for each partition, where sdXn is the partition being unmounted. 17 Getting Started with a Raspberry Pi Computer 6. Write the image file to the SD card with the following command: sudo dd if=filename.img of=/dev/sdX bs=4M 7. The process will take some time to write to the SD card, returning to the terminal prompt when complete. 8. Unmount the SD card before removing it from the computer using the following command: umount /dev/sdX1 The following steps should work for most of the versions of OS X: 1. Using your preferred web browser, download the Raspbian image and save it somewhere suitable. 2. Extract the file from the file manager or locate the folder in the terminal and unzip the .img file with the following command: unzip filename.zip 3. If you haven't already done so, insert your SD card into your card reader and plug it into your computer. 4. Use the diskutil list command and identify the disk# identifier for the SD card. Each partition will be displayed as disk#s1, disk#s2, and so on, where # will be 1, 2, 3, 4, and so on for the device ID. If rdisk# is listed, use this for faster writing (this uses a raw path and skips data buffering). 5. Ensure that the SD card is unmounted using the unmountdisk /dev/diskX command, where diskX is the device being unmounted. 6. Write the image file to the SD card with following command: sudo dd if=filename.img of=/dev/diskX bs=1M 7. The process will take some time to write to the SD card, returning to the terminal prompt when complete. 8. Unmount the SD card before removing it from the computer using the following command: unmountdisk /dev/diskX 18 Chapter 1 Refer to the following image: The boot process of a manually installed OS image Expanding the system to fit in your SD card A manually written image will be of a fixed size (usually made to fit the smallest-sized SD card possible). To make full use of the SD card, you will need to expand the system partition to fill the remainder of the SD card. This can be achieved using the Raspberry Pi Configuration tool. Select Expand Filesystem, as shown in the following screenshot: The Raspberry Pi Configuration tool 19 Getting Started with a Raspberry Pi Computer Accessing the RECOVERY/BOOT partition Windows and Mac OS X do not support the ext4 format, so when you read the SD card, only the File Allocation Table (FAT) partitions will be accessible. In addition, Windows only supports the first partition on an SD card, so if you've installed NOOBS, only the RECOVERY partition will be visible. If you've written your card manually, you will be able to access the BOOT partition. The data partition (if you installed one via NOOBS) and the root partition are in ext4 format and won't usually be visible on non-Linux systems. If you do need to read files from the SD card using Windows, a freeware program, Linux Reader (available at www.diskinternals.com/linuxreader) can provide read-only access to all of the partitions on the SD card. Access the partitions from the Raspberry Pi. To view the currently mounted partitions, use df, as shown in the following screenshot: The result of the df command To access the BOOT partition from within Raspbian, use the following command: cd /boot/ 20 Chapter 1 To access the RECOVERY or data partition, we have to mount it by performing the following steps: 1. Determine the name of the partition as the system refers to it by listing all the partitions, even the unmounted ones. The sudo fdisk -l command lists the partitions, as shown in the following screenshot: The partition table of a NOOBS install of Raspbian and data partition mmcblk0p1 (vfat) RECOVERY mmcblk0p2 (Extended partition) contains (root, data, BOOT) mmcblk0p5 (ext4) root mmcblk0p6 (vfat) BOOT mmcblk0p7 (ext4) SETTINGS If you have installed additional operating systems on the same card, the partition identifiers shown in the preceding table will be different. 2. Create a folder and set it as the mount point for the partition, as follows: For the RECOVERY partition, use the following command: mkdir ~/recovery sudo mount –t vfat /dev/mmcblk0p1 ~/recovery To ensure that they are mounted each time the system is started, perform the following steps: 1. Add the sudo mount commands to /etc/rc.local before exit 0. If you have a different username, you will need to change pi to match: sudo nano /etc/rc.local sudo mount -t vfat /dev/mmcblk0p1 /home/pi/recovery 2. Save and exit by pressing Ctrl + X, Y, and Enter. Commands added to /etc/rc.local will be run for any user who logs on to the Raspberry Pi. If you only want the drive to be mounted for the current user, the commands can be added to .bash_profile instead. 21 Getting Started with a Raspberry Pi Computer If you have to install additional operating systems on the same card, the partition identifiers shown here will be different. Using the tools to back up your SD card in case of failure You can use Win32 Disk Imager to make a full backup image of your SD card by inserting your SD card into your reader, starting the program, and creating a filename to store the image in. Simply click on the Read button instead to read the image from the SD card and write it to a new image file. To make a backup of your system, or to clone to another SD card using the Raspberry Pi, use the SD Card Copier (available from the desktop menu via the Accessories | SD Card Copier). Insert an SD card into a card reader into a spare USB port of the Raspberry Pi and select the new storage device, as shown in the following screenshot: SD Card Copier program Before continuing, the SD Card Copier will confirm that you wish to format and overwrite the target device and, if there is sufficient space, make a clone of your system. The dd command can similarly be used to back up the card, as follows: ff For Linux, replacing sdX with your device ID, use this command: sudo dd if=/dev/sdX of=image.img.gz bs=1M ff For OS X, replacing diskX with your device ID, use the following command: sudo dd if=/dev/diskX of=image.img.gz bs=1M ff You can also use gzip and split to compress the contents of the card and split them into multiple files if required for easy archiving, as follows: sudo dd if=/dev/sdX bs=1M | gzip –c | split –d –b 2000m – image. img.gz ff To restore the split image, use the following command: sudo cat image.img.gz* | gzip –dc | dd of=/dev/sdX bs=1M 22 Chapter 1 Networking and connecting your Raspberry Pi to the Internet via the LAN connector The simplest way to connect the Raspberry Pi to the Internet is by using the built-in LAN connection on the Model B. If you are using a Model A Raspberry Pi, a USB-to-LAN adapter can be used (refer to the There's more… section of the Networking and connecting your Raspberry Pi to the Internet via a USB Wi-Fi dongle recipe for details on how to configure this). Getting ready You will need access to a suitable wired network, which will be connected to the Internet, and a standard network cable (with an RJ45 type connector for connecting to the Raspberry Pi). How to do it… Many networks connect and configure themselves automatically using Dynamic Host Configuration Protocol (DHCP), which is controlled by the router or switch. If this is the case, simply plug the network cable into a spare network port on your router or network switch (or wall network socket if applicable). Alternatively, if a DHCP server is not available, you shall have to configure the settings manually (refer to the There's more… section for details). You can confirm this is functioning successfully with the following steps: 1. Ensure that the two LEDs on either side of the Raspberry Pi light up (the left orange LED indicates a connection and the green LED on the right shows activity by flashing). This will indicate that there is a physical connection to the router and the equipment is powered and functioning. 2. Test the link to your local network using the ping command. First, find out the IP address of another computer on the network (or the address of your router perhaps, often 192.168.0.1 or 192.168.1.254). Now, on the Raspberry Pi terminal, use the ping command (the -c 4 parameter is used to send just four messages; otherwise, press Ctrl + C to stop) to ping the IP address as follows: sudo ping 192.168.1.254 -c 4 3. Test the link to the Internet (this will fail if you usually connect to the Internet through a proxy server) as follows: sudo ping www.raspberrypi.org -c 4 23 Getting Started with a Raspberry Pi Computer 4. Finally, you can test the link back to the Raspberry Pi by discovering the IP address using hostname -I on the Raspberry Pi. You can then use the ping command on another computer on the network to ensure it is accessible (using the Raspberry Pi's IP address in place of www.raspberrypi.org). The Windows version of the ping command will perform five pings and stop automatically, and will not need the -c 4 option. If the aforementioned tests fail, you will need to check your connections and then confirm the correct configuration for your network. There's more… If you find yourself using your Raspberry Pi regularly on the network, you won't want to have to look up the IP address each time you want to connect to it. On some networks, you may be able to use the Raspberry Pi's hostname instead of its IP address (the default is raspberrypi). To assist with this, you may need some additional software such as Bonjour to ensure hostnames on the network are correctly registered. If you have an OS X Mac, you will have Bonjour running already. On Windows, you can either install iTunes (if you haven't got it) which also includes the service, or you can install it separately (via the Apple Bonjour Installer available from https://support.apple.com/kb/DL999). Then you can use the hostname, raspberrypi or raspberrypi.local, to connect to the Raspberry Pi over the network. If you need to change the hostname, then you can do so in the Raspberry Pi configuration tool, shown previously. Alternatively, you may find it helpful to fix the IP address to a known value by manually setting the IP address. However, remember to switch it back to use DHCP when connecting on another network. Some routers will also have an option to set a Static IP DHCP address, so the same address is always given to the Raspberry Pi (how this is set will vary on the router itself). Knowing your Raspberry Pi's IP address or using the hostname is particularly useful if you intend to use one of the remote access solutions described later on, which avoids the need for a display. Using built-in Wi-Fi and Bluetooth on the Raspberry Pi Many home networks provide a wireless network over Wi-Fi; if you have a Raspberry Pi 3, then you can make use of the on-board Broadcom Wi-Fi to connect to it. The Raspberry Pi 3 also supports Bluetooth, so you can connect most standard Bluetooth devices and use them like you would on any other computer. 24 Chapter 1 This method should also work for any supported USB Wi-Fi and Bluetooth devices, see the Networking and connecting your Raspberry Pi to the Internet via a USB Wi-Fi dongle recipe for extra help on identifying device and installing firmware (if required). Getting ready The latest version of Raspbian includes helpful utilities to quickly and easily configure your Wi-Fi and Bluetooth through the graphical interface. Note: If you need to configure the Wi-Fi via the command line, then see the Networking and connecting your Raspberry Pi to the Internet via a USB Wi-Fi dongle recipe for details. Wi-Fi and Bluetooth configuration applications You can use the built-in Bluetooth to connect a wireless keyboard, a mouse or even wireless speakers. This can be exceptionally helpful for projects where additional cables and wires are an issue, such as robotic projects, or when the Raspberry Pi is installed in hard-to-reach locations (acting as a server or security camera). How to do it… Connecting to your Wi-Fi network To configure your Wi-Fi connection, click on the networking symbol to list the local available Wi-Fi networks: Wi-Fi listing of the available access points in the area 25 Getting Started with a Raspberry Pi Computer Select the required network (for example, Demo) and if required enter your password (also known as Pre Shared Key): Providing the password for the access point After a short while, you should see that you have connected to the network and the icon will change to a Wi-Fi symbol. If you encounter problems, ensure you have the correct password/key. Successful connection to an access point That is it, as easy as that! You can now test your connection and ensure it is working by using the web browser to navigate to a website or by using the following command in the terminal: sudo ping www.raspberrypi.com Connecting to Bluetooth devices To start, we need to put the Bluetooth device into discoverable mode, by clicking on the Bluetooth icon and selecting Make Discoverable. You will also need to make the device you want to connect to discoverable and ready to pair; this may vary from device to device (such as pressing a pairing button). Set the Bluetooth as discoverable 26 Chapter 1 Next, select Add Device... and select the target device and Pair: Select and pair the required device The pairing process will then start; for example, the BTKB-71DB keyboard will need the pairing code 467572 to be entered onto the keyboard for the pairing to complete. Other devices may use default pairing codes, often set to 0000, 1111, 1234, or similar. Follow the instructions to pair the device with the required pairing code Once the process has completed, the device will be listed and will connect automatically each time the devices are present and booted. Configuring your network manually If your network does not include a DHCP server or it is disabled (typically, these are built into most modern ADSL/cable modems or routers), you may need to configure your network settings manually. 27 Getting Started with a Raspberry Pi Computer Getting ready Before you start, you will need to determine the network settings for your network. You will need to find out the following information from your router's settings or another computer connected to the network: ff IPv4 address: This address will need to be selected to be similar to other computers on the network (typically, the first three numbers should match, that is, 192.168.1.X if netmask is 255.255.255.0), but it should not already be used by another computer. However, avoid x.x.x.255 as the last address since this is reserved as a broadcast address. ff Subnet mask: This number determines the range of addresses the computer will respond to (for a home network, it is typically 255.255.255.0, which allows up to 254 addresses). This is also sometimes referred to as the netmask. ff Default gateway address: This address is usually your router's IP address, through which the computers connect to the Internet. ff DNS servers: The DNS server (Domain Name Service) converts names into IP addresses by looking them up. Usually, they will already be configured on your router, in which case you can use your router's address. Alternatively, your Internet Service Provider (ISP) may provide some addresses, or you can use Google's public DNS servers at the addresses 8.8.8.8 and 8.8.4.4. These are also called nameservers in some systems. For Windows, you can obtain this information by connecting to the Internet and running the following command: ipconfig /all 28 Chapter 1 Locate the active connection (usually called Local Area Connection 1 or similar if you are using a wired connection, or if you are using Wi-Fi, it is called wireless network connection) and find the information required, as follows: The ipconfig/all command shows useful information about your network settings For Linux and Mac OS X, you can obtain the required information with the following command (note that it is ifconfig rather than ipconfig): ifconfig The DNS servers are called nameservers and are usually listed in the resolv.conf file. You can use the less command as follows to view its contents (press Q to quit when you have finished viewing it): less /etc/resolv.conf How to do it… To set the network interface settings, edit /etc/network/interfaces using the following code: sudo nano /etc/network/interfaces 29 Getting Started with a Raspberry Pi Computer Now, perform the following steps: 1. We can add the details for our particular network, the IP address number we want to allocate to it, the netmask address of the network, and the gateway address, as follows: iface eth0 inet static address 192.168.1.10 netmask 255.255.255.0 gateway 192.168.1.254 2. Save and exit by pressing Ctrl + X, Y, and Enter. 3. To set the nameservers for DNS, edit /etc/resolv.conf using the following code: sudo nano /etc/resolv.conf 4. Add the addresses for your DNS servers as follows: nameserver 8.8.8.8 nameserver 8.8.4.4 5. Save and exit by pressing Ctrl + X, Y, and Enter. There's more… You can configure the network settings by editing cmdline.txt in the BOOT partition and adding settings to the startup command line with ip. The ip option takes the following form: ip=client-ip:nfsserver-ip:gw-ip:netmask:hostname:device:autoconf ff The client-ip option is the IP address you want to allocate to the Raspberry Pi ff The gw-ip option will set the gateway server address if you need to set it manually ff The netmask option will directly set the netmask of the network ff The hostname option will allow you to change the default raspberrypi hostname ff The device option allows you to specify a default network device if more than one network device is present ff The autoconf option allows the automatic configuration to be switched on or off 30 Chapter 1 Networking directly to a laptop or computer It is possible to connect the Raspberry Pi LAN port directly to a laptop or computer using a single network cable. This will create a local network link between the computers, allowing all the things you can do if connected to a normal network without the need for a hub or router, including connection to the Internet, if Internet Connection Sharing (ICS) is used as follows: Make use of the Raspberry Pi with just a network cable, a standard imaged SD card, and power ICS allows the Raspberry Pi to connect to the Internet through another computer. However, some additional configuration is required for the computers in order to communicate across the link, as the Raspberry Pi does not automatically allocate its own IP address. We will use the ICS to share a connection from another network link, such as a built-in Wi-Fi on a laptop. Alternatively, we can use a direct network link (refer to the Direct network link section under the There's more… section) if the Internet is not required or if the computer has only a single network adapter. Although this setup should work for most of the computers, some setups are more difficult than the others. For additional information, see www.pihardware.com/guides/ direct-network-connection. 31 Getting Started with a Raspberry Pi Computer Getting ready You will need the Raspberry Pi with power and a standard network cable. The Raspberry Pi Model B LAN chip includes Auto-MDIX (Automatic Medium-Dependent Interface Crossover). Removing the need to use a special crossover cable (a special network cable wired so that the transmit lines connect to receive lines for direct network links), the chip will decide and change the setup as required automatically. It may also be helpful to have a keyboard and monitor available to perform additional testing, particularly if this is the first time you have tried this. To ensure that you can restore your network settings to their original values, you should check whether it has a fixed IP address or the network is configured automatically. To check the network settings on Windows 10, perform these steps: 1. Open Settings from the start menu, then select Network & Internet, then Ethernet, and click on Change adapter options from the list of Related Settings. To check the network settings on Windows 7 and Vista, perform the following steps: 1. Open Network and Sharing Center from the Control Panel and click on Change adapter settings on the left-hand side. 2. To check the network settings on Windows XP, open Network Connections from the Control Panel. 3. Find the item that relates to your wired network adapter (by default, this is usually called Ethernet or Local Area Connection, as shown in the following screenshot): Locating your wired network connection 32 Chapter 1 4. Right-click on its icon and click on Properties. A dialog box will appear, as shown in this screenshot: Selecting the TCP/IP properties and checking the settings 5. Select the item called Internet Protocol (TCP/IP) or Internet Protocol Version 4 (TCP/IPv4) if there are two versions (the other is Version 6), and click on the Properties button. 6. You can confirm that your network is set by using automatic settings or a specific IP address (if so, take note of this address and the remaining details as you may want to revert the settings at a later point). 33 Getting Started with a Raspberry Pi Computer To check the network settings on Linux, perform the following steps: 1. Open up the Network Settings dialog box and select Configure Interface. Refer to the following screenshot: Linux Network Settings dialog box 2. Ensure that if any settings are manually set, you take note of them so that you can restore them later if you want. To check the network settings on Mac OS X, perform the following steps: 1. Open System Preferences and click on Networks. You can then confirm whether the IP address is allocated automatically (using DHCP) or not. 2. Ensure that if any settings are manually set you take note of them so you can restore them later if you want to. Refer to the following screenshot: 34 Chapter 1 OS X Network Settings dialog box If you just need to access or control the Raspberry Pi without an Internet connection, refer to the Direct network link section in the There's more…section. How to do it… First, we need to enable ICS on our network devices. In this case, we will be sharing the Internet, which is available on Wireless Network Connection through the Ethernet connection to the Raspberry Pi. 35 Getting Started with a Raspberry Pi Computer For Windows, perform these steps: 1. Return to the list of network adapters, right-click on the connection that links to the Internet (in this case, the WiFi or Wireless Network Connection device), and click on Properties. Right-click on your wireless device and select Properties 2. At the top of the window, select the second tab (in Windows XP, it is called Advanced; in Windows 7 and Windows 10, it is called Sharing), as shown in the following screenshot: Selecting the TCP/IP properties and noting the allocated IP address 36 Chapter 1 3. In the Internet Connection Sharing section, check the box for Allow other network users to connect through this computer's Internet connection (if present, use the drop-down box to select the Home networking connection: option as Ethernet or Local Area Connection). Click on OK and confirm whether you previously had a fixed IP address set for Local Area Connection. For Mac OS X, to enable the ICS, perform the following steps: 1. Click on System Preferences and then click on Sharing. 2. Click on Internet Sharing and select the connection from which we want to share the Internet (in this case, it will be the Wi-Fi AirPort). Then select the connection that we will connect the Raspberry Pi to (in this case, Ethernet). For Linux to enable the ICS, perform the following steps: 1. From the System menu, click on Preferences and then on Network Connections. Select the connection you want to share (in this case, Wireless) and click on Edit or Configure. In the IPv4 Settings tab, change the Method option to Shared to other computers. The IP address of the network adapter will be the Gateway IP address to be used on the Raspberry Pi, and be assigned an IP address within the same range (matching except the last number). For instance, if the computer's wired connection now has 192.168.137.1, the Gateway IP of the Raspberry Pi will be 192.168.137.1 and its own IP address might be set to 192.168.137.10. Fortunately, thanks to updates in the operating system, Raspbian will now automatically allocate a suitable IP address to join the network and set the gateway appropriately. However, unless we have a screen attached to the Raspberry Pi or scan for devices on our network, we do not know what IP address the Raspberry PI has given itself. Fortunately (as mentioned in the Networking and connecting your Raspberry Pi to the Internet via the LAN connector recipe in the There's more… section), Apple's Bonjour software will automatically ensure hostnames on the network are correctly registered. As stated previously, if you have an OSX Mac you will have Bonjour running already. On Windows you can either install iTunes, or you can install it separately (available from https://support.apple. com/kb/DL999). By default, the hostname raspberrypi can be used. We are now ready to test the new connection, as follows: 1. Connect the network cable to the Raspberry Pi and the computer's network port, and then power up the Raspberry Pi, ensuring that you have reinserted the SD card if you previously removed it. To reboot the Raspberry Pi if you edited the file there, use sudo reboot to restart it. 2. Allow a minute or two for the Raspberry Pi to fully power up. We can now test the connection. 37 Getting Started with a Raspberry Pi Computer 3. From the connected laptop or computer, test the connection by pinging with the hostname of the Raspberry Pi, as shown in the following command (on Linux or OS X, add -c 4 to limit to four messages or press Ctrl + C to exit): ping raspberrypi Hopefully, you will find you have a working connection and receive replies from the Raspberry Pi. If you have a keyboard and screen connected to the Raspberry Pi, you can perform the following steps: 1. You can ping the computer in return (for example, 192.168.137.1) from the Raspberry Pi terminal as follows: sudo ping 192.168.137.1 -c 4 2. You can test the link to the Internet by using ping to connect to a well-known website as follows, assuming you do not access the Internet through a proxy server: sudo ping www.raspberrypi.org -c 4 If all goes well, you will have full Internet available through your computer to the Raspberry Pi, allowing you to browse the web as well as update and install new software. If the connection fails, perform the following steps: 1. Repeat the process, ensuring that the first three sets of numbers match with the Raspberry Pi and the network adapter IP addresses. 2. You can also check that when the Raspberry Pi powers up, the correct IP address is being set using the following command: hostname -I 3. Check your firewall settings to ensure it is not blocking internal network connections. How it works… When we enable ICS on the primary computer, the operating system will automatically allocate a new IP address to the computer. Once connected and powered up, the Raspberry Pi will set itself to a compatible IP address and use the primary computer IP address as an Internet Gateway. By using Apple Bonjour, we are able to use the raspberrypi hostname to connect to the Raspberry Pi from the connected computer. Finally, we check whether the computer can communicate over the direct network link to the Raspberry Pi, back the other way, and also through to the Internet. 38 Chapter 1 There's more… If you do not require the Internet on the Raspberry Pi, or your computer only has a single network adapter, we can still connect the computers together through a direct network link. Refer to the following diagram: Connecting and using the Raspberry Pi with just a network cable, a standard imaged SD card, and power Direct network link For a network link to work between two computers, they need to be using the same address range. The allowable address range is determined by the subnet mask (for example, 255.255.0.0 or 255.255.255.0 would mean all IP addresses should be the same except for the last two or just the last number in the IP address; otherwise, they will be filtered). To use a direct link without enabling ICS, check the IP settings of the adapter you are going to connect to and determine whether it is automatically allocated or fixed to a specific IP address. Most PCs connected directly to another computer will allocate an IP address in the range 169.254.X.X (with a subnet mask of 255.255.0.0). However, we must ensure that the network adaptor is set to Obtain an IP address automatically. For the Raspberry Pi to be able to communicate through the direct link, it needs to have an IP address in the same address range, 169.254.X.X. As mentioned before, the Raspberry Pi will automatically give itself a suitable IP address and connect to the network. Therefore, assuming we have Apple Bonjour (see previously for details), we only need to know the hostname given to the Raspberry Pi (raspberrypi). 39 Getting Started with a Raspberry Pi Computer See also If you don't have a keyboard or screen connected to the Raspberry Pi, you can use this network link to remotely access the Raspberry Pi just as you would on a normal network (just use the new IP address you have set for the connection). Refer to the Connecting remotely to the Raspberry Pi over the network using VNC and Connecting remotely to the Raspberry Pi over the network using SSH (and X11 Forwarding) recipes. There is lots of additional information available on my website, https://pihw.wordpress. com/guides/direct-network-connection, including additional troubleshooting tips and several other ways to connect to your Raspberry Pi without needing a dedicated screen and keyboard. Networking and connecting your Raspberry Pi to the Internet via a USB Wi-Fi dongle By adding a USB Wi-Fi dongle to the Raspberry Pi's USB port, even models without built-in WiFi can connect to and use the Wi-Fi network. Getting ready You will need to obtain a suitable USB Wi-Fi dongle; and in some cases, you may require a powered USB hub (this will depend on the hardware version of the Raspberry Pi you have and the quality of your power supply). General suitability of USB Wi-Fi dongles will vary depending on the chipset that is used inside and the level of Linux support available. You may find that some USB Wi-Fi dongles will work without installing additional drivers (in which case you can jump to configuring it for the wireless network). A list of supported Wi-Fi adapters is available at http://elinux.org/RPi_USB_Wi-Fi_ Adapters. You will need to ensure that your Wi-Fi adapter is also compatible with your intended network; for example, it supports the same types of signals 802.11bgn and the encryptions WEP, WPA, and WPA2 (although most networks are backward compatible). You will also need the following details of your network: ff Service set identifier (SSID): This is the name of your Wi-Fi network and should be visible if you use the following command: sudo iwlist scan | grep SSID ff 40 Encryption type and key: This value will be None, WEP, WPA, or WPA2, and the key will be the code you normally enter when you connect your phone or laptop to the wireless network (sometimes, it is printed on the router). Chapter 1 You will require a working internet connection (that is, wired Ethernet) in order to download the required drivers. Otherwise, you may be able to locate the required firmware files (they will be the .deb files), and copy them to the Raspberry Pi (that is, via a USB flash drive; the drive should be automatically mounted if you are running in desktop mode). Copy the file to a suitable location and install it with the following command: sudo apt-get install firmware_file.deb How to do it… This task has two stages; first, we identify and install firmware for the Wi-Fi adapter, and then we need to configure it for the wireless network. We will try to identify the chipset of your Wi-Fi adapter (the part that handles the connection); this may not match the actual manufacturer of the device. An approximate list of supported firmware can be found with this command: sudo apt-cache search wireless firmware This will produce results similar to the following output (disregarding any results without firmware in the package title): atmel-firmware - Firmware for Atmel at76c50x wireless networking chips. firmware-atheros - Binary firmware for Atheros wireless cards firmware-brcm80211 - Binary firmware for Broadcom 802.11 wireless cards firmware-ipw2x00 - Binary firmware for Intel Pro Wireless 2100, 2200 and 2915 firmware-iwlwifi - Binary firmware for Intel PRO/Wireless 3945 and 802.11n cards firmware-libertas - Binary firmware for Marvell Libertas 8xxx wireless cards firmware-ralink - Binary firmware for Ralink wireless cards firmware-realtek - Binary firmware for Realtek wired and wireless network adapters libertas-firmware - Firmware for Marvell's libertas wireless chip series (dummy package) zd1211-firmware - Firmware images for the zd1211rw wireless driver To find out the chipset of your wireless adapter, plug the Wi-Fi-adapter into Raspberry Pi, and from the terminal, run the following command: dmesg | grep 'Product:\|Manufacturer:' 41 Getting Started with a Raspberry Pi Computer This command stitches together two commands into one. First, dmesg displays the message buffer of the kernel (this is an internal record of system events that have occurred since power on, such as detected USB devices). You can try the command on its own to observe the complete output. The | (pipe) sends the output to the grep command, grep 'Product:\|Manuf' checks it and only returns lines that contain Product or Manuf (so we should get a summary of any items that are listed as Product and Manufacturer). If you don't find anything or want to see all your USB devices, try grep 'usb' instead. This should return something similar to the following output (in this case, I've got a ZyXEL device, which has a ZyDAS chipset (a quick Google search reveals that zd1211-firmware is for ZyDAS devices): [ 1.893367] usb usb1: Product: DWC OTG Controller [ 1.900217] usb usb1: Manufacturer: Linux 3.6.11+ dwc_otg_hcd [ 3.348259] usb 1-1.2: Product: ZyXEL G-202 [ 3.355062] usb 1-1.2: Manufacturer: ZyDAS Once you have identified your device and the correct firmware, you can install it, as you would for any other package available through apt-get (where zd1211-firmware can be replaced with your required firmware). This is shown in the following command: sudo apt-get install zd1211-firmware Remove and reinsert the USB Wi-Fi dongle to allow it to be detected and the drivers loaded. We can now test if the new adapter is correctly installed with ifconfig. The output is shown as follows: wlan0 IEEE 802.11bg Mode:Managed Retry ESSID:off/any Access Point: Not-Associated long limit:7 RTS thr:off Tx-Power=20 dBm Fragment thr:off Power Management:off The command will show the network adapters present on the system. For Wi-Fi, this is usually as wlan0, or wlan1, or so on if you have installed more than one. If not, double-check the selected firmware, and perhaps try an alternative or check on the site for troubleshooting tips. 42 Chapter 1 Once we have the firmware installed for the Wi-Fi adapter, we will need to configure it for the network we wish to connect. We can use the GUI as shown in the previous recipe, or we can manually configure it through the terminal as shown in the following steps: 1. We will need to add the wireless adapter to the list of network interfaces, which is set in /etc/network/interfaces, as follows: sudo nano -c /etc/network/interfaces Using the previous wlan# value in place of wlan0 if required, add the following command: allow-hotplug wlan0 iface wlan0 inet manual wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf When the changes have been made, save and exit by pressing Ctrl + X, Y, and Enter. 2. We will now store the Wi-Fi network settings of our network in the wpa_ supplicant.conf file (don't worry if your network doesn't use the wpa encryption; it is just the default name for the file): sudo nano -c /etc/wpa_supplicant/wpa_supplicant.conf It should include the following: ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev update_config=1 country=GB The network settings can be written within this file as follows (that is, if the SSID is set as theSSID): If no encryption is used, use this code: network={ ssid="theSSID" key_mgmt=NONE } With the WEP encryption (that is, if the WEP key is set as theWEPkey), use the following code: network={ ssid="theSSID" key_mgmt=NONE wep_key0="theWEPkey" } 43 Getting Started with a Raspberry Pi Computer For the WPA or WPA2 encryption (that is, if the WPA key is set as theWPAkey), use the following code: network={ ssid="theSSID" key_mgmt=WPA-PSK psk="theWPAkey" } 3. You can enable the adapter with the following command (again, replace wlan0 if required): sudo ifup wlan0 Use the following command to list the wireless network connections: iwconfig You should see your wireless network connected with your SSID listed as follows: wlan0 IEEE 802.11bg ESSID:"theSSID" Mode:Managed Frequency:2.442 GHz 00:24:BB:FF:FF:FF Bit Rate=48 Mb/s Retry Access Point: Tx-Power=20 dBm long limit:7 RTS thr:off Fragment thr:off Power Management:off Link Quality=32/100 Rx invalid nwid:0 Signal level=32/100 Rx invalid crypt:0 Tx excessive retries:0 Invalid misc:15 Rx invalid frag:0 Missed beacon:0 If not, adjust your settings and use sudo ifdown wlan0 to switch off the network interface, and then sudo ifup wlan0 to switch it back on. This will confirm that you have successfully connected to your Wi-Fi network. 4. Finally, we will need to check whether we have access to the Internet. Here, we have assumed that the network is automatically configured with DHCP and no proxy server is used. If not, refer to the Connecting to the Internet through a proxy server recipe. Unplug the wired network cable, if still connected, and see if you can ping the Raspberry Pi website, as follows: sudo ping www.raspberrypi.org 44 Chapter 1 If you want to quickly know the IP address currently in use by the Raspberry Pi, you can use hostname -I. Or to find out which adapter is connected to which IP address, use ifconfig. There's more… The Model A version of the Raspberry Pi does not have a built-in network port; so in order to get a network connection, a USB network adapter will have to be added (either a Wi-Fi dongle, as explained in the preceding section, or a LAN-to-USB adapter, as described in the following section). Using USB wired network adapters Just like USB Wi-Fi, the adapter support will depend on the chipset used and the drivers available. Unless the device comes with Linux drivers, you may have to search the Internet to obtain the suitable Debian Linux drivers. If you find a suitable .deb file, you can install it with the following command: sudo apt-get install firmware_file.deb Also check using ifconfig, as some devices will be supported automatically, appear as eth1 (or eth0 on Model A), and be ready for use immediately. Connecting to the Internet through a proxy server Some networks, such as workplaces or schools, often require you to connect to the Internet through a proxy server. Getting ready You will need the address of the proxy server you are trying to connect to, including the username and password if one is required. You should confirm that the Raspberry Pi is already connected to the network and that you can access the proxy server. Use the ping command to check this as follows: ping proxy.address.com -c 4 If this fails (you get no responses), you will need to ensure your network settings are correct before continuing. 45 Getting Started with a Raspberry Pi Computer How to do it… Create a new file using nano as follows (if there is already some content in the file, you can add the code at the end): sudo nano -c ~/.bash_profile To allow basic web browsing through programs such as midori while using a proxy server, you can use the following script: function proxyenable { # Define proxy settings PROXY_ADDR="proxy.address.com:port" # Login name (leave blank if not required): LOGIN_USER="login_name" # Login Password (leave blank to prompt): LOGIN_PWD= #If login specified - check for password if [[ -z $LOGIN_USER ]]; then #No login for proxy PROXY_FULL=$PROXY_ADDR else #Login needed for proxy Prompt for password -s option hides input if [[ -z $LOGIN_PWD ]]; then read -s -p "Provide proxy password (then Enter):" LOGIN_PWD echo fi PROXY_FULL=$LOGIN_USER:$LOGIN_PWD@$PROXY_ADDR fi #Web Proxy Enable: http_proxy or HTTP_PROXY environment variables export http_proxy="http://$PROXY_FULL/" export HTTP_PROXY=$http_proxy export https_proxy="https://$PROXY_FULL/" export HTTPS_PROXY=$https_proxy export ftp_proxy="ftp://$PROXY_FULL/" export FTP_PROXY=$ftp_proxy #Set proxy for apt-get sudo cat </dev/null Acquire::http::proxy "http://$PROXY_FULL/"; Acquire::ftp::proxy "ftp://$PROXY_FULL/"; Acquire::https::proxy "https://$PROXY_FULL/"; EOF #Remove info no longer needed from environment unset LOGIN_USER LOGIN_PWD PROXY_ADDR PROXY_FULL echo Proxy Enabled 46 Chapter 1 } function proxydisable { #Disable proxy values, apt-get and git settings unset http_proxy HTTP_PROXY https_proxy HTTPS_PROXY unset ftp_proxy FTP_PROXY sudo rm /etc/apt/apt.conf.d/80proxy echo Proxy Disabled } Once done, save and exit by pressing Ctrl + X, Y, and Enter. The script is added to the user's own .bash_profile file, which is run when that particular user logs in. This will ensure that the proxy settings are kept separately for each user. If you want all users to use the same settings, you can add the code to /etc/rc.local instead (this file must have exit 0 at the end). How it works… Many programs that make use of the Internet will check for the http_proxy or HTTP_PROXY environment variables before connecting. If they are present, they will use the proxy settings to connect through. Some programs may also use the HTTPS and FTP protocols, so we can set the proxy setting for them here too. If a username is required for the proxy server, a password will be prompted for. It is generally not recommended to store your passwords inside scripts unless you are confident that no one else will have access to your device (either physically or through the Internet). The last part allows any programs that execute using the sudo command to use the proxy environment variables while acting as the super user (most programs will try accessing the network using normal privileges first, even if running as a super user, so it isn't always needed). 47 Getting Started with a Raspberry Pi Computer There's more… We also need to allow the proxy settings to be used by some programs, which use super user permissions while accessing the network (this will depend on the program; most don't need this). We need to add the commands into a file stored in /etc/sudoers.d/ by performing the following steps: It is important to use visudo here, as it ensures the permissions of the file are created correctly for the sudoers directory (read only by the root user). 1. Use the following command to open a new sudoer file: sudo visudo -f /etc/sudoers.d/proxy 2. Enter the following text in the file (on a single line): Defaults env_keep += "http_proxy HTTP_PROXY https_proxy HTTPS_ PROXY ftp_proxy FTP_PROXY" 3. Once done, save and exit by pressing Ctrl + X, Y, and Enter; don't change the proxy.tmp filename (this is normal for visudo; it will change it to proxy when finished). 4. If prompted What now?, there is an error in the command. Press X to exit without saving and retype the command! 5. After a reboot (using sudo reboot), you will be able to use the following commands to enable and disable the proxy respectively: proxyenable proxydisable Connecting remotely to the Raspberry Pi over the network using VNC Often, it is preferable to remotely connect to and control the Raspberry Pi across the network, for instance, using a laptop or desktop computer as a screen and keyboard, or while the Raspberry Pi is connected elsewhere, perhaps even connected to some hardware it needs to be close to. VNC is just one way in which you can remotely connect to the Raspberry Pi. It will create a new desktop session that will be controlled and accessed remotely. The VNC session here is separate from the one that may be active on the Raspberry Pi's display. 48 Chapter 1 Getting ready Ensure that your Raspberry Pi is powered up and connected to the Internet. We will use the Internet connection to install a program using apt-get. This is a program that allows us to find and install applications directly from the official repositories. How to do it… First, we need to install the TightVNC server on the Raspberry Pi with the following commands. It is advisable to run an update command first to get the latest version of the package you want to install as follows: sudo apt-get update sudo apt-get install tightvncserver Accept the prompt to install and wait until it completes. To start a session, use the following command to start a session: vncserver :1 The first time you run this, it will ask you to enter a password (of no more than eight characters) to access the desktop (you will use this when you connect from your computer). The following message should confirm that a new desktop session has been started: New 'X' desktop is raspberrypi:1 If you do not already know the IP address of the Raspberry Pi, use hostname –I and take note of it. Next, we need to run a VNC client, VNC Viewer is suitable program, which is available at http://www.realvnc.com/ and should work on Windows, Linux, and OS X. When you run VNC Viewer, you will be prompted for the Server address and Encryption type. Use the IP address of your Raspberry Pi with :1. That is, for the IP address 192.168.1.69, use the 192.168.1.69:1 address. You can leave the Encryption type as Off or Automatic. Depending on your network, you may be able to use the hostname; the default is raspberrypi, that is raspberrypi:1. You may have a warning about not having connected to the computer before or having no encryption. You should enable encryption if you are using a public network or if you are performing connections over the Internet (to stop others from being able to intercept your data). 49 Getting Started with a Raspberry Pi Computer There's more… You can add options to the command line to specify the resolution and also the color depth of the display. The higher the resolution and color depth (can be adjusted to use 8 to 32 bits per pixel to provide low or high color detail), the more data has to be transferred through the network link. If you find the refresh rate a little slow, try reducing these numbers as follows: vncserver :1 –geometry 1280x780 –depth 24 To allow the VNC server to start automatically when you switch on, you can add the vncserver command to .bash_profile (this is executed each time the Raspberry Pi starts). Use the nano editor as follows (the -c option allows the line numbers to be displayed): sudo nano -c ~/.bash_profile Add the following line to the end of the file: vncserver :1 The next time you power up, you should be able to remotely connect using VNC from another computer. Connecting remotely to the Raspberry Pi over the network using SSH (and X11 Forwarding) An SSH (Secure SHell) is often the preferred method for making remote connections, as it allows only the terminal connections and typically requires fewer resources. An extra feature of SSH is the ability to transfer the X11 data to an X Windows server running on your machine. This allows you to start programs that would normally run on the Raspberry Pi desktop, and they will appear in their own Windows on the local computer, as follows: 50 Chapter 1 X11 Forwarding on a local display X Forwarding can be used to display applications, which are running on the Raspberry Pi, on a Windows computer. Getting ready If you are running the latest version of Raspbian, SSH and X11 Forwarding will be enabled by default (otherwise, double-check the settings explained in the How it works… section). How to do it… Linux and OS X have built-in support for X11 Forwarding; but if you are using Windows, you will need to install and run the X Windows server on your computer. Download and run xming from the Xming site (http://sourceforge.net/projects/ xming/). Install xming, following the installation steps, including the installation of PuTTY if you don't have it already. You can also download PuTTY separately from http://www.putty.org/. Next, we need to ensure that the SSH program we use has X11 enabled when we connect. For Windows, we shall use PuTTY to connect to the Raspberry Pi. 51 Getting Started with a Raspberry Pi Computer In the PuTTY Configuration dialog box, navigate to Connection | SSH | X11 and tick the checkbox for X11 Forwarding. If you leave the X display location option blank, it will assume the default Server 0:0 as follows (you can confirm the server number by moving your mouse over the Xming icon in the system tray when it is running): Enabling X11 Forwarding within the PuTTY configuration Enter the IP address of the Raspberry Pi in the Session settings (you may also find that you can use the Raspberry Pi's hostname here instead; the default hostname is raspberrypi). Save the setting using a suitable name, RaspberryPi, and click on Open to connect to your Raspberry Pi. 52 Chapter 1 You are likely to see a warning message pop up stating you haven't connected to the computer before (this allows you to check whether you have everything right before continuing). Opening an SSH connection to the Raspberry Pi using PuTTY For OS X or Linux, click on Terminal to open a connection to the Raspberry Pi. To connect with the default pi username, with an IP address of 192.168.1.69, use the following command; the -X option enables X11 Forwarding: ssh -X pi@192.168.1.69 All being well, you should be greeted with a prompt for your password (remember the default value for the pi user is raspberry). Ensure that you have Xming running by starting the Xming program from your computer's Start menu. Then, in the terminal window, type a program that normally runs within the Raspberry Pi desktop, such as leafpad or scratch. Wait a little while and the program should appear on your computer's desktop (if you get an error, you have probably forgotten to start Xming, so run it and try again). 53 Getting Started with a Raspberry Pi Computer How it works… X Windows and X11 is what provides the method by which the Raspberry Pi (and many other Linux-based computers) can display and control graphical Windows as part of a desktop. For X11 Forwarding to work over a network connection, we need both SSH and X11 Forwarding enabled on the Raspberry Pi. Perform the following steps: 1. To switch on (or off) SSH, you can access the Raspberry Pi Configuration program under the Preferences menu on the Desktop and click on SSH within the Interfaces tab, as shown in the following screenshot (SSH is often enabled by default for most distributions to help allow remote connections without needing a monitor to configure it): The Interfaces tab in the Raspberry Pi Configuration tool 2. Ensure that X11 Forwarding is enabled on the Raspberry Pi (again most distributions now have this enabled by default). 3. Use nano with the following command: sudo nano /etc/ssh/sshd_config 4. Look for a line in the /etc/ssh/sshd_config file that controls X11 Forwarding and ensure that it says yes (with no # sign before it), as follows: X11Forwarding yes 54 Chapter 1 5. Save if required by pressing Ctrl + X, Y, and Enter and reboot (if you need to change it) as follows: sudo reboot There's more… SSH and X 11 Forwarding is a convenient way to control the Raspberry Pi remotely; we will explore some additional tips on how to use it effectively in the following sections. Running multiple programs with X11 Forwarding If you want to run an X program, but still be able to use the same terminal console for other stuff, you can run the command in the background with & as follows: leafpad & Just remember that the more programs you run, the slower everything will get. You can switch to the background program by typing fg and check for background tasks with bg. Running as a desktop with X11 Forwarding You can even run a complete desktop session through X11, although it isn't particularly user-friendly and VNC will produce better results. To achieve this, you have to use lxsession instead of startx (in the way you would normally start the desktop from the terminal). An alternative is to use lxpanel, which provides the program menu bar from which you can start and run programs from the menu as you would on the desktop. Running PyGame and Tkinter with X11 Forwarding You can get the following error (or similar) when running the PyGame or Tkinter scripts: _tkinter.TclError: couldn't connect to display "localhost:10.0" In this case, use the following command to fix the error: sudo cp ~/.Xauthority ~root/ Sharing the home folder of the Raspberry Pi with SMB When you have the Raspberry Pi connected to your network, you can access the home folder by setting up file sharing; this makes it much easier to transfer files and provides a quick and easy way to back up your data. Server Message Block (SMB) is a protocol that is compatible with Windows file sharing, OS X, and Linux. 55 Getting Started with a Raspberry Pi Computer Getting ready Ensure that you have the Raspberry Pi powered and running with a working connection to the Internet. You will also need another computer on the same local network to test the new share. How to do it… First, we need to install samba, a piece of software that handles folder sharing in a format that is compatible with Windows sharing methods. Ensure that you use update as follows to obtain the latest list of available packages: sudo apt-get update sudo apt-get install samba The install will require around 20 MB of space and take a few minutes. Once the install has completed, we can make a copy of the configuration file as follows to allow us to restore to defaults if needed: sudo cp /etc/samba/smb.conf /etc/samba/smb.conf.backup sudo nano /etc/samba/smb.conf Scroll down and find the section named Authentication; change the # user line to security = user. security = As described in the file, this setting ensures that you have to enter your username and password for the Raspberry Pi in order to access the files (this is important for shared networks). Find the section called Share Definitions and [homes], and change the read only = yes line to read only = no. This will allow us to view and also write files to the shared home folder. Once done, save and exit by pressing Ctrl + X, Y, and Enter. If you have changed the default user from pi to something else, substitute it in the following instructions. Now, we can add pi (the default user) to use samba: sudo pdbedit -a -u pi 56 Chapter 1 Now, enter a password (you can use the same password as your login or select a different one, but avoid using the default Raspberry password, which would be very easy for someone to guess). Restart samba to use the new configuration file, as follows: sudo /etc/init.d/samba restart [ ok ] Stopping Samba daemons: nmbd smbd. [ ok ] Starting Samba daemons: nmbd smbd. In order to test, you will need to know either the Raspberry Pi's hostname (the default hostname is raspberrypi) or its IP address. You can find both of these with the following command: hostname For the IP address, add -I: hostname –I On another computer on the network, enter the \\raspberrypi\pi address in the explorer path. Depending on your network, the computer should locate the Raspberry Pi on the network and prompt for a username and password. If it can't find the share using the hostname, you can use the IP address directly, where 192.168.1.69 should be changed to match the IP address \\192.168.1.69\pi. Keeping the Raspberry Pi up to date The Linux image used by the Raspberry Pi is often updated to include enhancements, fixes, and improvements to the system, as well as adding support for new hardware or changes made to the latest board. Many of the packages that you install can be updated too. This is particularly important if you plan on using the same system image on another Raspberry Pi board (particularly a newer one) as older images will lack support for any wiring changes or alternative RAM chips. New firmware should work on older Raspberry Pi boards, but older firmware may not be compatible with the latest hardware. Fortunately, you need not reflash your SD card every time there is a new release, since you can update it instead. Getting ready You will need to be connected to the Internet in order to update your system. It is always advisable to make a backup of your image first (and at a minimum, take a copy of your important files). 57 Getting Started with a Raspberry Pi Computer You can check your current version of firmware with the uname -a command, as follows: Linux raspberrypi 4.4.9-v7+ #884 SMP Fri May 6 17:28:59 BST 2016 armv7l GNU/Linux The GPU firmware can be checked using the /opt/vc/bin/vcgencmd version command, as follows: May 6 2016 13:53:23 Copyright (c) 2012 Broadcom version 0cc642d53eab041e67c8c373d989fef5847448f8 (clean) (release) This is important if you are using an older version of firmware (preNovember 2012) on a newer board since the original Model B board was only 254 MB RAM. Upgrading allows the firmware to make use of the extra memory if available. The free -h command will detail the RAM available to the main processor (the total RAM is split between the GPU and ARM cores) and will give the following output: total used free shared buffers cached 925M 224M 701M 7.1M 14M 123M -/+ buffers/cache: 86M 839M 0B 99M Mem: Swap: 99M You can then recheck the preceding output following a reboot to confirm that they have been updated (although they may have already been the latest). How to do it… Before running any upgrades or installing any packages, it is worth ensuring you have the latest list of packages in the repository. The update command gets the latest list of available software and versions: sudo apt-get update If you just want to obtain an upgrade of your current packages, upgrade will bring them all up to date: sudo apt-get upgrade To ensure that you are running the latest release of Raspbian, you can run dist-upgrade (be warned; this can take an hour or so depending on the amount that needs to be upgraded). This will perform all the updates that upgrade will perform but will also remove redundant packages and clean up: sudo apt-get dist-upgrade 58 Chapter 1 Both methods will upgrade the software, including the firmware used at boot and startup (bootcode.bin and start.elf). There's more… You will often find that you will want to perform a clean installation of your setup; however, this will mean you will have to install everything from scratch. To avoid this, I developed the Pi-Kitchen project (https://github.com/PiHw/Pi-Kitchen), based on the groundwork of Kevin Hill. This aims to provide a flexible platform for creating customized setups that can be automatically deployed to an SD card. Pi Kitchen allows the Raspberry Pi be configured before powering up The Pi-Kitchen allows a range of flavors to be configured, which can be selected from the NOOBS menu. Each flavor consists of a list of recipes, each providing a specific function or feature to the final operating system. Recipes can range from setting up custom drivers for Wi-Fi devices, to mapping shared drives on your network, to providing a fully functional web server out of the box, all combining to make your required setup. This project is in beta, developed as a proof of concept, but once you have everything configured, it can be incredibly useful to deploy fully working setups directly onto an SD card. Ultimately, the project could be combined with Kevin Hill's advanced version of NOOBS, called PINN (short for PINN Is Not NOOBS), which aims to allow extra features for advanced users, such as allowing operating systems and configurations to be stored on your network or on an external USB memory stick. 59 2 Starting with Python Strings, Files, and Menus In this chapter, we will cover the following topics: ff Working with text and strings ff Using files and handling errors ff Creating a boot-up menu ff Creating a self-defining menu Introduction In this chapter, we discuss how to use Python to perform some basic encryption by scrambling letters. This will introduce some basic string manipulation, user input, progressing on to creating reusable modules, and graphical user interfaces. To follow, we will create some useful Python scripts that can be added to run as the Raspberry Pi boots or an easy-to-run command that will provide quick shortcuts to common or frequently-used commands. Taking this further, we will make use of threading to run multiple tasks and introduce classes to define multiple objects. As it is customary to start any programming exercise with a Hello World example, we will kick off with that now. 61 Starting with Python Strings, Files, and Menus Create the hellopi.py file using nano, as follows: nano -c hellopi.py Within our hellopi.py file, add the following code: #!/usr/bin/python3 #hellopi.py print ("Hello Raspberry Pi") When done, save and exit (Ctrl + X, Y, and Enter). To run the file, use the following command: python3 hellopi.py Congratulations, you have created your first program! Your result should be similar to the following screenshot: The Hello Raspberry Pi output Working with text and strings A good starting point for Python is to gain an understanding of basic text handling and strings. A string is a block of characters stored together as a value. As you will learn, they can be viewed as a simple list of characters. We will create a script to obtain the user's input, use string manipulation to switch around the letters, and print out a coded version of the message. We will then extend this example by demonstrating how encoded messages can be passed between parties without revealing the encoding methods, while also showing how to reuse sections of the code within other Python modules. Getting ready You can use most text editors to write Python code. They can be used directly on the Raspberry Pi or remotely through VNC or SSH. 62 Chapter 2 The following are a few text editors that are available with the Raspberry Pi: ff nano: This text editor is available at the terminal and includes syntax highlighting and line numbers (with the -c option). Refer to the following screenshot: The nano command-line editor ff IDLE3: This Python editor includes the syntax highlighting feature, context help, and will run scripts directly from the editor (on pressing F5). This program requires X-Windows (the Debian desktop) or X11-forwarding to run remotely. We will be using Python 3 throughout the book, so ensure that you run IDLE3 (rather than IDLE), which will use Python 3 to run the scripts, as shown in the following screenshot: The IDLE3 Python editor 63 Starting with Python Strings, Files, and Menus ff Geany: This text editor provides an Integrated Development Environment (IDE) that supports a range of programming languages, syntax highlighting, auto completion, and easy code navigation. This is a feature-rich editor, but can be difficult to use for beginners and may sometimes be slow when running on the Raspberry Pi. Again, you will need to run this editor with the Debian desktop or X11-forwarding. Refer to the following screenshot: The Geany IDE To install Geany, use the following command and then run Geany from the Programming menu item: sudo apt-get install geany To ensure Geany uses Python 3 when you click on the Execute button (to run your scripts), you will need to change the build commands. Load hellopi.py and then click on the Build menu and select Set Build Commands. In the window that appears, as shown in the following screenshot, change python to python3 in the Compile and Execute sections. Python is always compiled automatically when it is run (producing the temporary .pyc files), so you don't need to use the Compile button, except maybe to check the syntax of the code: 64 Chapter 2 The Geany Build command settings for Python 3 If you have the home directory of the Raspberry Pi shared across the network (refer to Sharing the home folder of the Raspberry Pi with SMB recipe in Chapter 1, Getting Started with a Raspberry Pi Computer), you can edit files on another computer. However, note that if you use Windows, you must use an editor that supports Linux line endings, such as Notepad++ (you should not use the standard Notepad program). To create a space for your Python scripts, we will add a python_scripts folder to your home directory with the following command: mkdir ~/python_scripts 65 Starting with Python Strings, Files, and Menus Now you can open this folder and list the files whenever you need to, using the following commands: cd ~/python_scripts ls You can use the Tab key to help complete commands in the terminal, for example, typing cd ~/pyt and then pressing the Tab key will finish the command for you. If there are multiple options that start with pyt, pressing the Tab key again will list them. To repeat or edit older commands, use the up and down arrow keys to switch between older and newer commands as required. How to do it… Create the encryptdecrypt.py script as follows: #!/usr/bin/python3 #encryptdecrypt.py #Takes the input_text and encrypts it, returning the result def encryptText(input_text,key): input_text=input_text.upper() result = "" for letter in input_text: #Ascii Uppercase 65-90 Lowercase 97-122 (Full range 32-126) ascii_value=ord(letter) #Exclude non-characters from encryption if (ord("A") > ascii_value) or (ascii_value > ord("Z")): result+=letter else: #Apply encryption key key_value = ascii_value+key #Ensure we just use A-Z regardless of key if not((ord("A")) < key_val < ord("Z")): key_val = ord("A")+(key_val-ord("A"))\ %(ord("Z")-ord("A")+1) #Add the encoded letter to the result string result+=str(chr(key_value)) return result #Test function def main(): 66 Chapter 2 print ("Please enter text to scramble:") #Get user input try: user_input = input() scrambled_result = encryptText(user_input,10) print ("Result: " + scrambled_result) print ("To un-scramble, press enter again") input() unscrambled_result = encryptText(scrambled_result,-10) print ("Result: " + unscrambled_result) except UnicodeDecodeError: print ("Sorry: Only ASCII Characters are supported") main() #End Within the There's more… section of this recipe, we will change main() to the following code: if __name__=="__main__": main() If you want to skip the section, ensure that you include this change in the encryptdecrypt.py file, as we will use it later. How it works… The preceding script implements a very basic method to scramble the text using a simple form of character substitution called the Caesar cipher. Named after the Roman emperor Julius Caesar, who originally used this method to send secret orders to his armies. The file defines two functions, encryptText() and main(). When the script is running, the main() function obtains the user's input using the input() command. The result is stored as a string in the user_input variable (the command will wait until the user has pressed the Enter key before continuing), as follows: user_input = input() The input() function will not handle non-ASCII characters, so we use try… except to handle this case, which will cause UnicodeDecodeError to be raised. For more information about using try…except, refer to the Using files and handling errors recipe of this chapter. 67 Starting with Python Strings, Files, and Menus We will call the encryptText() function with two arguments, the text to be encrypted, and the key. After the text has been encrypted, the result is printed: scrambled_result = encryptText(user_input,10) print ("Result: " + scrambled_result) Finally, we will use input() to wait for the user input again (in this case, a prompt to press Enter; any other input is ignored). Now, we will unscramble the text by reversing the encryption by calling encryptText() again, but with a negative version of the key, and displaying the result, which should be the original message. The encryptText() function performs a simple form of encryption by taking the letters in the message and substituting each letter with another in the alphabet (determined by counting from the number of letters specified by the encryption key). In this way, the letter A will become C when the encryption key is 2. To simplify the process, the function converts all characters to uppercase. This allows us to use the ASCII character set to translate each character into numbers easily; the letter A is represented by 65 and Z by 90. This is achieved with input_text=input_text.upper() and then by using ord(letter) to convert to an ASCII value, which gives us a number representation of the character. ASCII is a standard that maps the numbers 0 to 254 (an 8-bit value) and commonly used characters and symbols: A B C D E F G H I J K L M 65 66 67 68 69 70 71 72 73 74 75 76 77 N O P Q R S T U V W X Y Z 78 79 80 81 82 83 84 85 86 87 88 89 90 ASCII table for upper case letters Next, we will ensure that we have an empty string where we can build our result (result = "") and we will also set our encryption key to our key value. The input_text variable contains our string, which is stored as a list (this is similar to an array) of letters. We can access each item in the list using input_text[0] for the first item and so on; however, Python also allows us to loop through a list using for…in, accessing each item in turn. The for letter in input_text: line allows us to break up input_text by looping through it for each item inside (in this case, the letters in the string) and also setting the letter equal to that item. So if input_text equaled HELLO, it would run all the code that is indented under the command five times; each time, the letter variable would be set to H, E, L, L, and finally O. This allows us to read each letter separately, process it, and add the new encrypted letter to the result string. 68 Chapter 2 The next part, if (ord("A") > ascii_value) or (ascii_value > ord("Z")):, checks to see if the character we are looking at is not between A and Z, which means it is probably a number or punctuation mark. In this case, we will exclude the character from the encryption (passing the character directly to the result string unchanged). If the letter is between A and Z, we can add the value of our encryption key to the value of our letter to obtain our new encoded letter. That is, for an encryption key of 10, we end up with the following set of letters in the output: Input Letter: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z Output Letter: K L M N O P Q R S T U V W X Y Z A B C D E F G H I J As we want the encrypted message to be easy to write out, we have limited the output to be between A and Z. So, if the letter starts as X, we want to it to wrap around and continue counting from A. We can achieve this by using the % (modulus) function, which gives us the remainder value if we divide a number by another. So, if X is 24 and we add 10, we get 34. The value of 34%26 (where 26 is the total number of letters) is 8. Counting 8 from A, we reach H. However, in ASCII, the letter A is the number 65, so we will remove this offset from key_ value and then add it back once we have the modulus value. The following code ensures that we limit the ASCII values to be between A and Z: #Ensure we just use A-Z regardless of key if not((ord("A")) < key_value < ord("Z")): key_value = ord("A")+(key_value-ord("A"))\ %(ord("Z")-ord("A")+1) Essentially, if the value is not between the values for A or Z, then we will allow the value to wrap around (by calculating the modulus using the total number of letters between A and Z, which is 26). This also works if the key is larger than 26 and if we are counting the opposite way too, for instance, if the encryption key was negative, and therefore, the decryption key positive. Finally, we can convert key_value back into a letter by using the chr() and str() functions and adding it to the result string. Note that we use \ to split the code into another line, it doesn't affect the calculation. Python can be very fussy about splitting lines, in some cases you can find a natural break in the code and separate the line with a carriage return, however other times we have to force a line break using the \ symbol. Of course, given very little time, such a simple encryption method could easily be broken. Remember that there are only 25 possible combinations to choose from before the result of the encryption is repeated (multiples of 26 will result in no encryption at all). 69 Starting with Python Strings, Files, and Menus There's more… You can try this simple experiment. Currently, with this basic form of encryption, you will supply the method and key to anyone you wish to read your message. However, what happens if you want to send a secure transmission without sending the method and key? The answer is to send the same message back and forth three times as demonstrated in the following diagram: We do not need to exchange encryption keys with the other person The first time, we will encrypt it and send it over to the other party. They will then encrypt it again with their own encryption and send it back. The message at this stage has two layers of encryption applied to it. We can now remove our encryption and return it. Finally, they will receive the message with just their encryption, which they can remove and read the message. Just remember that there are only 25 useful encryption combinations with the Caesar cipher, so it is possible that they could decrypt the message by accident. We can make use of our previous file as a module using the import command as follows: import encryptdecrypt as ENC 70 Chapter 2 This will allow access to any function inside the encryptdecrypt file using ENC as the reference. When such a file is imported, it will run any code that would normally be run; in this case, the main() function. To avoid this, we can change the call to main() only to occur when the file is run directly. If the file is run directly, Python will set __name__ to the "__main__" global attribute. By using the following code, we can reuse the functions in this Python script in other scripts without running any other code: if __name__=="__main__": main() Create the keypassing.py script using the following code in the same directory as encryptdecrypt.py: #!/usr/bin/python3 #keypassing.py import encryptdecrypt as ENC KEY1 = 20 KEY2 = 50 print ("Please enter text to scramble:") #Get user input user_input = input() #Send message out encodedKEY1 = ENC.encryptText(user_input,KEY1) print ("USER1: Send message encrypted with KEY1 (KEY1): " + encodedKEY1) #Receiver encrypts the message again encodedKEY1KEY2 = ENC.encryptText(encodedKEY1,KEY2) print ("USER2: Encrypt with KEY2 & returns it (KEY1+KEY2): " + encodedKEY1KEY2) #Remove the original encoding encodedKEY2 = ENC.encryptText(encodedKEY1KEY2,-KEY1) print ("USER1: Removes KEY1 & returns with just KEY2 (KEY2): " + encodedKEY2) #Receiver removes their encryption message_result = ENC.encryptText(encodedKEY2,-KEY2) print ("USER2: Removes KEY2 & Message received: " + message_result) #End 71 Starting with Python Strings, Files, and Menus On running the preceding script, we can see that the other person doesn't need to know the encryption key that we are using, and anyone who intercepts the message will not be able to see its contents. The script produces the following output: Please enter text to scramble: "A message to a friend." USER1: Send message encrypted with KEY1 (KEY1): U GYMMUAY NI U ZLCYHX. USER2: Encrypt with KEY2 & returns it (KEY1+KEY2): S EWKKSYW LG S XJAWFV. USER1: Removes KEY1 & returns with just KEY2 (KEY2): Y KCQQYEC RM Y DPGCLB. USER2: Removes KEY2 & Message received: A MESSAGE TO A FRIEND. This method is known as the three-pass protocol, developed by Adi Shamir in 1980 (http://en.wikipedia.org/wiki/Three-pass_protocol). One particular disadvantage of this method is that it is possible for a third party to intercept the messages (the so-called man-in-the-middle attack) and characterize the encryption method by inserting known values and analyzing the responses. Using files and handling errors In addition to easy string handling, Python allows you to read, edit, and create files easily. So, by building upon the previous scripts, we can make use of our encryptText() function to encode complete files. Reading and writing to files can be quite dependent on factors that are outside of the direct control of the script, such as whether the file that we are trying to open exists or the filesystem has space to store a new file. Therefore, we will also take a look at how to handle exceptions and protect operations that may result in errors. Getting ready The following script will allow you to specify a file through the command line, which will be read and encoded to produce an output file. Create a small text file named infile.txt and save it so that we can test the script. It should include a short message similar to the following: This is a short message to test our file encryption program. How to do it… Create the fileencrypt.py script using the following code: #!/usr/bin/python3 #fileencrypt.py 72 Chapter 2 import sys #Imported to obtain command line arguments import encryptdecrypt as ENC #Define expected inputs ARG_INFILE=1 ARG_OUTFILE=2 ARG_KEY=3 ARG_LENGTH=4 def covertFile(infile,outfile,key): #Convert the key text to an integer try: enc_key=int(key) except ValueError: print ("Error: The key %s should be an integer value!" % (key)) #Code put on to two lines else: try: #Open the files with open(infile) as f_in: infile_content=f_in.readlines() except IOError: print ("Unable to open %s" % (infile)) try: with open(outfile,'w') as f_out: for line in infile_content: out_line = ENC.encryptText(line,enc_key) f_out.writelines(out_line) except IOError: print ("Unable to open %s" % (outfile)) print ("Conversion complete: %s" % (outfile)) finally: print ("Finish") #Check the arguments if len(sys.argv) == ARG_LENGTH: print ("Command: %s" %(sys.argv)) covertFile(sys.argv[ARG_INFILE], sys.argv[ARG_OUTFILE], sys. argv[ARG_KEY]) else: print ("Usage: fileencrypt.py infile outfile key") #End 73 Starting with Python Strings, Files, and Menus To run the script, use the following command (here, infile can be any text file we want to encrypt, outfile is our encrypted version, and key is the key value we wish to use): python3 fileencrypt.py infile outfile key For example, to encrypt infile.txt and output it as encrypted.txt using 30 as the key, use the following command: python3 fileencrypt.py infile.txt encrypted.txt 30 To view the result, use less encrypted.txt. Press Q to exit. To decrypt encrypted.txt and output it as decrypted.txt using -30 as the key, use the following command: python3 fileencrypt.py encrypted.txt decrypted.txt -30 To view the result, use less decrypted.txt. Press Q to exit. How it works… The script requires us to use arguments that are provided on the command line. We will access them by importing the Python module called sys. Just like we did earlier, we will also import our encryptdecrypt module using the import command. We will use the as part to allow us to reference it using ENC. Next, we will set values to define what each command-line argument will represent. When you run it, you will see that sys.argv[] is a list of values shown in the following array: ['fileencrypt.py', 'infile.txt', 'encrypted.txt', '30'] So, the input file is at the index 1 in the list (indexing always starts at 0), then the output file, and finally, the key, with the total number of arguments being ARG_LENGTH=4. Next, we will define the convertFile() function, which we will call in a minute from the next block of code. To avoid errors, we will check whether the length of the sys.argv value matches the expected number of arguments from the command line. This will ensure that the user has supplied us with enough, and we don't try to reference items in the sys.argv[] list that don't exist. Otherwise, we will return a short message explaining what we are expecting. We will now call the convertFile() function using the command-line values and making use of Python's built-in exception handling features to ensure that errors are responded to accordingly. The try…except code allows you to try running some code and handle any exceptions (errors) within the program itself, rather than everything coming to a sudden stop. 74 Chapter 2 The try code is accompanied by the following four optional sections: ff except ValueError: – When an error occurs, a specific type of exception can be specified and handled with the action, depending on the error we wish to handle (that is, for ValueError, we could check whether the value is a float value and convert it to an integer or prompt for a new one). Multiple exceptions can be caught using except (ValueError,IOError) as required. ff except: – This is a catch-all case where any other exceptions that we haven't handled can be dealt with. For situations where the code may be called from other places, we may also want to raise the exception again using the raise command so that it can be dealt with by other parts of the program (for instance, as part of the GUI, we can warn the user that the input was not correct without needing to do so at this stage). Typically, you should either deal with a specific exception or ensure that you raise it again so that the particular error is visible on a failure; if not handled at all, Python will report it on the terminal along with the trace to the function where it occurred. ff else: – This section of code is always executed if the try code was successful and there was no exception raised; however, any errors in this code will not be handled by the try…except section it is part of. ff finally: – This code is always executed, regardless of whether an exception was raised or the try code ran without problems. If you are familiar with other languages, you will find try…except similar to try…catch, and raise and throw as equivalents. Dealing with exceptions can be quite an art form; however, making your code able to handle problems gracefully and effectively is all part of good design. In many cases, catching the situations where things go wrong is just as important as performing the intended function successfully. If there is no problem with converting the key argument into an integer, we will continue to open the input file specified and read the contents into the infile_content list. This will contain the contents of the file split into separate lines as a list. In this example, we will use a slightly different method to display values within the print statement. Consider the following code as an example: print ("Error: The key %s should be an integer value!" %(key)) This allows us to use the %s symbol to determine where the key value is printed and also to specify the format (%s is a string). For numerical values, such as floats and integers, we can use %d to display integers, %f for floats, or even %.4f to limit the value to four decimal places. 75 Starting with Python Strings, Files, and Menus You may have noticed that we opened the file using the with…as…: section. This is a special way to open a file, which will ensure that it is closed once it has finished (even if there is an error). Refer to the following code: try: #Open the files with open(infile) as f_in: infile_content=f_in.readlines() except IOError: print ("Unable to open %s" % (infile)) This is equivalent to the following: try: f_in = open(infile) try: infile_content=f_in.readlines() finally: f_in.close() except IOError: print ("Unable to open %s" % (infile)) If there is an exception in opening the file (if it doesn't exist, for example, it will raise IOError), we can flag to the user that there was a problem with the filename/path provided. We will also use except: on its own to deal with any other problems that we may have with the file, such as the encoding type or non-text based files. Next, we will open a file for our output using 'w' to open it as a writable file. If it doesn't exist, it will create a new file; otherwise, it will overwrite the file. We will also have the option to append to the file instead, using 'a'. We will step through each item in infile_content, converting each line by passing it through our ENC.encryptText() function and writing the line to the f_out file. Once again, when we finish the with…as…: section, the file is closed and the conversion is complete. Creating a boot-up menu We will now apply the methods introduced in the previous scripts and reapply them to create a menu that we can customize to present a range of quick-to-run commands and programs. 76 Chapter 2 How to do it… Create the menu.py script using the following code: #!/usr/bin/python3 #menu.py from subprocess import call filename="menu.ini" DESC=0 KEY=1 CMD=2 print ("Start Menu:") try: with open(filename) as f: menufile = f.readlines() except IOError: print ("Unable to open %s" % (filename)) for item in menufile: line = item.split(',') print ("(%s):%s" % (line[KEY],line[DESC])) #Get user input running = True while(running): user_input = input() #Check input, and execute command for item in menufile: line = item.split(',') if (user_input == line[KEY]): print ("Command: " + line[CMD]) #call the script #e.g. call(["ls", "-l"]) commands = line[CMD].rstrip().split() print (commands) running = False #Only run command is one if available if len(commands): call(commands) if (running==True): print ("Key not in menu.") print ("All Done.") #End 77 Starting with Python Strings, Files, and Menus Create a menu.ini file that will contain the following menu items and commands: Start Desktop,d,startx Show IP Address,i,hostname -I Show CPU speed,s,cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_ freq Show Core Temperature,t,sudo /opt/vc/bin/vcgencmd measure_temp Exit,x, You can add your own commands to the list, creating your own custom start-up menu. The script will assume the menu.ini file is formatted correctly, so if you experience an error (for example ErrorIndex) it may be because the file is not as expected (such as missing commas or blank lines). We could use except ErrorIndex: to handle any errors, however we are better off highlighting there is a problem within the input file so that it can be fixed instead. How it works… In order to execute other programs from within a Python script, we need to use the call command. This time, we only wish to use the call part of the subprocess module, so we can simply use from subprocess import call. This just imports the part we need. We will open the file and read all the lines into a menufile list. We can then process each item (or line of the file) using item.split(','), which will create a new list consisting of each section of the line divided by the ',' symbol, as follows: line = ['Start Desktop', 'd', 'startx\n'] As shown by the print statement, we can now access each section independently, so we can print the key we need to press for a specific command and the description of the command. Once we have printed the entire menu of commands, we will wait for the user's input. This is done inside a while loop; it will continue to run until we set the condition inside running to False. This means that if an invalid key is pressed, we can enter another key until a command is selected or the exit item is used. We will then check the input key to see if it matches the allocated key for the menu item, as follows: user_input == line[KEY] If there is a match, we will extract the command we wish to call. The call command requires a command and its arguments to be a list, so we will use .split() to break up the command part into a list (where each space in the command is a new item in the list). Also note that there is /n after startx, this is the end of the line character from the menu.ini file. We will remove this first using .rstrip(), which removes any whitespace (spaces, tabs, or line endings) from the end of a string. 78 Chapter 2 Once the command is formatted into a list of arguments, we will set running to False (so the while loop will not enter another loop), execute our command, and finish the script. If the user selects x, there will be no commands set, allowing us to exit the menu without calling anything. The script produces a small menu of options, as shown in the following: Start Menu: (d):Start Desktop (i):Show IP Address (s):Show CPU speed (t):Show Core Temperature (x):Exit g Key not in menu. i Command: hostname -I ['hostname', '-I'] All Done. There's more… To make the script run each time, we will start the Raspberry Pi; we can call it from .bash_profile, which is a bash script that runs when the user's profile is loaded. Create or edit the file as follows: nano -c ~/.bash_profile Add the following commands (assuming menu.py is located in the /home/pi/python_ scripts directory): cd /home/pi/python_scripts python3 menu.py When done, save and exit (Ctrl + X, Y, and Enter). The next time you power up your Raspberry Pi, you will have a menu to run your favorite commands from, without needing to remember them. 79 Starting with Python Strings, Files, and Menus You can also run Python scripts directly, without the python3 command, making them executable, as follows: chmod +x menu.py Now type ./menu.py and the script will run using the program defined within the file by the first line, as follows: #!/usr/bin/python3 Creating a self-defining menu While the previous menu is very useful for defining the most common commands and functions we may use when running the Raspberry Pi, we will often change what we are doing or develop scripts to automate complex tasks. To avoid the need to continuously update and edit the menu.ini file, we can create a menu that can list the installed scripts and dynamically build a menu from it, as shown in the following screenshot: A menu of all the Python scripts in the current directory How to do it… Create the menuadv.py script using the following code: #!/usr/bin/python3 #menuadv.py import os from subprocess import call SCRIPT_DIR="." #Use current directory SCRIPT_NAME=os.path.basename(__file__) print ("Start Menu:") 80 Chapter 2 scripts=[] item_num=1 for files in os.listdir(SCRIPT_DIR): if files.endswith(".py"): if files != SCRIPT_NAME: print ("%s:%s"%(item_num,files)) scripts.append(files) item_num+=1 running = True while (running): print ("Enter script number to run: 1-%d (x to exit)" % (len(scripts))) run_item = input() try: run_number = int(run_item) if len(scripts) >= run_number > 0: print ("Run script number:" + run_item) commands = ["python3",scripts[run_number-1]] print (commands) call(commands) running = False except ValueError: #Otherwise, ignore invalid input if run_item == "x": running = False print ("Exit") #End How it works… This script allows us to take a different approach. Rather than predefining a list of commands or applications, we can simply keep a folder of useful scripts and scan it to create a list to pick from. In this case, the menu will just list Python scripts and call them without any commandline options. To be able to access the list of files in a directory, we can use the os module's os.listdir() function. This function allows us to specify a directory and it will return a list of the files and directories within it. Using SCRIPT_DIR="." will allow us to search the current directory (the one the script is being run from). We can specify an absolute path (that is, "//home/pi/python_ scripts"), a relative path (that is, "./python_scripts_subdirectory"), or navigate from the current directory to others in the structure (that is, "../more_scripts", where the .. symbol will move up a level from the current directory and then into the more_scripts directory if it existed). 81 Starting with Python Strings, Files, and Menus If the directory does not exist, an exception (OSError) will be raised. Since this menu is intended to simply run and display the list, we are better off letting the exception cause an error and stop the script. This will encourage the user to fix the directory rather than try to handle the error (perhaps by prompting for another path each time). It will also be easier for the user to locate and correct the path when the script isn't running. We will also get the name of the script using os.path.basename(__file__), this allows us to later exclude the menuadv.py script from the list options. We will create an empty scripts list and ensure that we initialize item_num to 1. Now, we will call os.listdir(SCRIPT_DIR) directly within a for…in loop so that we can process each directory or filename returned by it. Next, we can check the end of each item using the endswith() function (another useful string function), which allows us to look for a specific ending to the string (in this case, the ending for Python scripts). At this point, we can also exclude the menuadv.py script from the list, if found. We print the name of the script along with item_num and add it to the script list, finally incrementing item_num so that it is correct for the next item. We will now prompt the user to enter the relevant script number (between 1 and the total number of scripts) and wait for the user input from input(). The script will check for a valid input. If it is a number, it will stay in the try section, and we can then check whether the number is in the correct range (one of the listed script numbers). If correct, the script is called using ['python3', 'scriptname.py'] and the call() function as before. If the input is not a number (for example, x), it will raise the ValueError exception. Within the ValueError exception, we can check whether x was pressed and exit the while loop by setting running to False (otherwise, the loop will reprint the prompt and wait for new input). The script is now complete. You can adjust the preceding script to support other types of scripts, if required. Simply add other file extensions, such as .sh, to the scripts list and call using sh or bash instead of python3. There's more… We can extend this example further by placing all our useful scripts in a single place and adding the menu script to the path. 82 Chapter 2 Alternative script locations While not entirely necessary (by default, the script will look in the current directory), it will be useful to create a suitable location to keep your scripts that you would like to use with the menu. This can be a location within your home folder (~ is short for the home folder path, which is /home/pi by default). An example is shown in the following command line: mkdir ~/menupy cd ~/menupy To copy files, you can use cp sourcefile targetfile. If you use the -r option, it will also create the directory if it doesn't exist. To move or rename the files, use mv sourcefile targetfile. To delete the files, use rm targetfile. You must use the -r option to delete a directory. Just ensure that if the script is not within the same location, the path is updated for SCRIPT_DIR to refer to the required location. Adding scripts to PATH As before, we could add this script to a start-up file, such as .bash_profile, and have the menu appear when the user logs in to the Raspberry Pi. Alternatively, we can place such scripts into a folder such as /home/pi/bin, in which we can include the global value call PATH. The PATH settings are a list of directories that scripts and programs will check when trying to locate a file that isn't in the current directory (typically, installed programs and software, but also common configuration files and scripts). This will allow us to run the script regardless of what directory we are currently in. We can see the current PATH settings using the following command: echo $PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/ games:/usr/games The actual contents of the PATH setting will depend on the initial settings of the distribution you are using and also on the applications you have installed. If /home/pi/bin isn't included, we can temporarily add to this until the next boot with the following command: PATH=/home/pi/bin:$PATH We can also add this to .bash_profile to set it every time for the current user, as follows: PATH=$HOME/bin:$PATH export PATH 83 Starting with Python Strings, Files, and Menus The next time we reboot, the PATH settings will be (for a user with the name pi) as follows: /home/pi/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/ bin:/usr/local/games:/usr/games When items are automatically located through PATH, it can be difficult to find a specific version of a file or program. To overcome this, use whereis before the filename/command, and it will list all the locations where it can be found. Finally, if you do move the script to the bin directory, ensure that you update the path in os.listdir("//home/pi/bin") to locate and list the scripts you wish to display in the menu. 84 3 Using Python for Automation and Productivity In this chapter, we will cover the following topics: ff Using Tkinter to create graphical user interfaces ff Creating a graphical Start menu application ff Displaying photo information in an application ff Organizing your photos automatically Introduction Until now, we have focused purely on command-line applications; however, there is much more to the Raspberry Pi than just the command line. By using graphical user interfaces (GUIs), it is often easier to obtain input from a user and provide feedback in a more natural way. After all, we continuously process multiple inputs and outputs all the time, so why limit ourselves to the procedural format of the command line when we don't have to? 85 Using Python for Automation and Productivity Fortunately, Python can support this. Much like other programming languages, such as Visual Basic and C/C++/C#, this can be achieved using prebuilt objects that provide standard controls. We will use a module called Tkinter which provides a good range of controls (also referred to as widgets) and tools for creating graphical applications. First, we will take our previous example, the encryptdecrypt.py module discussed in the How to do it… section in the Working with text and strings recipe in Chapter 2, Starting with Python Strings, Files, and Menus, and demonstrate how useful modules can be written and reused in a variety of ways. This is a test of good coding practice. We should aim to write code that can be tested thoroughly and then reused in many places. Next, we will extend our previous examples by creating a small graphical Start menu application to run our favorite applications from. Then, we will explore using classes within our applications to display and then to organize photos. Using Tkinter to create graphical user interfaces We will create a small GUI to allow the user to enter information, and the program can then be used to encrypt and decrypt it. Getting ready You will need to ensure that you have completed the instructions in the There's more… section of the Working with text and strings recipe in Chapter 2, Starting with Python Strings, Files, and Menus, where we created the reusable encryptdecrypt.py module. You must ensure that this file is placed in the same directory as the following script. 86 Chapter 3 Since we are using Tkinter (one of many available add-ons of Python), we need to ensure that it is installed. It should be installed by default on the standard Raspbian image. We can confirm it is installed by importing it from the Python prompt, as follows: python3 >>> import tkinter If it is not installed, an ImportError exception will be raised, in which case you can install it using the following command (use Ctrl + Z to exit the Python prompt): sudo apt-get install python3-tk If the module did load, you can use the following command to read more about the module (use Q to quit when you are done reading): >>>help(tkinter) You can also get information about all the classes, functions, and methods within the module using the following command: >>>help(tkinter.Button) The following dir command will list any valid commands or variables that are in the scope of the module: >>>dir(tkinter.Button) You will see that our own modules will have the information about the functions marked by triple quotes; this will show up if we use the help command. The command line will not be able to display the graphical displays created in this chapter, so you will have to start the Raspberry Pi desktop (using the command startx), or if you are using it remotely, ensure you have X11 Forwarding enabled and an X server running (see Chapter 1, Getting Started with a Raspberry Pi Computer). How to do it… We will use the tkinter module to produce a GUI for the encryptdecrypt.py script we wrote in the previous chapter. To generate the GUI we will create the following tkencryptdecrypt.py script: #!/usr/bin/python3 #tkencryptdecrypt.py import encryptdecrypt as ENC import tkinter as TK def encryptButton(): 87 Using Python for Automation and Productivity encryptvalue.set(ENC.encryptText(encryptvalue.get(), keyvalue.get())) def decryptButton(): encryptvalue.set(ENC.encryptText(encryptvalue.get(), -keyvalue.get())) #Define Tkinter application root=TK.Tk() root.title("Encrypt/Decrypt GUI") #Set control & test value encryptvalue = TK.StringVar() encryptvalue.set("My Message") keyvalue = TK.IntVar() keyvalue.set(20) prompt="Enter message to encrypt:" key="Key:" label1=TK.Label(root,text=prompt,width=len(prompt),bg='green') textEnter=TK.Entry(root,textvariable=encryptvalue, width=len(prompt)) encryptButton=TK.Button(root,text="Encrypt",command=encryptButton) decryptButton=TK.Button(root,text="Decrypt",command=decryptButton) label2=TK.Label(root,text=key,width=len(key)) keyEnter=TK.Entry(root,textvariable=keyvalue,width=8) #Set layout label1.grid(row=0,columnspan=2,sticky=TK.E+TK.W) textEnter.grid(row=1,columnspan=2,sticky=TK.E+TK.W) encryptButton.grid(row=2,column=0,sticky=TK.E) decryptButton.grid(row=2,column=1,sticky=TK.W) label2.grid(row=3,column=0,sticky=TK.E) keyEnter.grid(row=3,column=1,sticky=TK.W) TK.mainloop() #End Run the script using the following command: python3 tkencryptdecrypt 88 Chapter 3 How it works… We start by importing two modules; the first is our own encryptdecrypt module and the second is the tkinter module. To make it easier to see which items have come from where, we use ENC/TK. If you want to avoid the extra reference, you can use from import * to refer to the module items directly. The encryptButton() and decryptButton() functions will be called when we click on the Encrypt and Decrypt buttons; they are explained in the following sections. The main Tkinter window is created using the Tk() command, which returns the main window where all the widgets/controls can be placed. We will define six controls as follows: ff Label: This displays the prompt Enter message to encrypt: ff Entry: This provides a textbox to receive the user's message to be encrypted ff Button: This is an Encrypt button to trigger the message to be encrypted ff Button: This is a Decrypt button to reverse the encryption ff Label: This displays the Key: field to prompt the user for an encryption key value ff Entry: This provides a second textbox to receive values for the encryption keys These controls will produce a GUI similar to the one shown in the following screenshot: The GUI to encrypt/decrypt messages Let's take a look at the first label1 definition: label1=TK.Label(root,text=prompt,width=len(prompt),bg='green') All controls must be linked to the application window; hence, we have to specify our Tkinter window root. The text used for the label is set by text; in this case, we have set it to a string named prompt, which has been defined previously with the text we require. We also set the width to match the number of characters of the message (while not essential, it provides a neater result if we add more text to our labels later), and finally, we set the background color using bg='green'. 89 Using Python for Automation and Productivity Next, we define the text Entry box for our message: textEnter=TK.Entry(root,textvariable=encryptvalue, width=len(prompt)) We will define textvariable—a useful way to link a variable to the contents of the box— which is a special string variable. We could access the text directly using textEnter. get(), but we shall use a Tkinter StringVar() object instead to access it indirectly. If required, this will allow us to separate the data we are processing from the code that handles the GUI layout. The enycrptvalue variable automatically updates the Entry widget it is linked to whenever the .set() command is used (and the .get() command obtains the latest value from the Entry widget). Next, we have our two Button widgets, Encrypt and Decrypt, as follows: encryptButton=TK.Button(root,text="Encrypt",command=encryptButton) decryptButton=TK.Button(root,text="Decrypt",command=decryptButton) In this case, we can set a function to be called when the Button widget is clicked by setting the command attribute. We can define the two functions that will be called when each button is clicked. In the following code snippet, we have the encryptButton() function, which will set the encryptvalue StringVar that controls the contents of the first Entry box. This string is set to the result we get by calling ENC.encryptText() with the message we want to encrypt (the current value of encryptvalue) and the keyvalue variable. The decrypt() function is exactly the same, except we make the keyvalue variable negative to decrypt the message: def encryptButton(): encryptvalue.set(ENC.encryptText(encryptvalue.get(), keyvalue.get())) We then set the final Label and Entry widgets in a similar way. Note that textvariable can also be an integer (numerical value) if required, but there is no built-in check to ensure that only numbers can be entered. You will get a ValueError exception when the .get() command is used. After we have defined all the widgets to be used in the Tkinter window, we have to set the layout. There are three ways to define the layout in Tkinter: place, pack, and grid. The place layout allows us to specify the positions and sizes using exact pixel positions. The pack layout places the items in the window in the order that they have been added in. The grid layout allows us to place the items in a specific layout. It is recommended that you avoid the place layout wherever possible since any small change to one item can have a knock-on effect on the positions and sizes of all the other items; the other layouts account for this by determining their positions relative to the other items in the window. 90 Chapter 3 We will place the items as laid out in the following screenshot: Grid layout for the Encrypt/Decrypt GUI The positions of first two items in the GUI are set using the following code: label1.grid(row=0,columnspan=2,sticky= TK.E+TK.W) textEnter.grid(row=1,columnspan=2,sticky= TK.E+TK.W) We can specify that the first Label and Entry box will span both columns (columnspan=2), and we can set the sticky values to ensure they span right to the edges. This is achieved by setting both the TK.E for the east and TK.W for the west sides. We'd use TK.N for the north and TK.S for the south sides if we needed to do the same vertically. If the column value is not specified, the grid function defaults to column=0. The other items are similarly defined. The last step is to call TK.mainloop(), which allows Tkinter to run; this allows the buttons to be monitored for clicks and Tkinter to call the functions linked to them. Creating a graphical application – Start menu The example in this recipe shows how we can define our own variations of the Tkinter objects to generate custom controls and dynamically construct a menu with them. We will also take a quick look at using threads to allow other tasks to continue to function while a particular task is being executed. 91 Using Python for Automation and Productivity Getting ready To view the GUI display, you will need a monitor displaying the Raspberry Pi desktop, or need to be connected to another computer running the X server. How to do it… To create a graphical Start menu application, create the following graphicmenu.py script: #!/usr/bin/python3 # graphicmenu.py import tkinter as tk from subprocess import call import threading #Define applications ["Display name","command"] leafpad = ["Leafpad","leafpad"] scratch = ["Scratch","scratch"] pistore = ["Pi Store","pistore"] app_list = [leafpad,scratch,pistore] APP_NAME = 0 APP_CMD = 1 class runApplictionThread(threading.Thread): def __init__(self,app_cmd): threading.Thread.__init__(self) self.cmd = app_cmd def run(self): #Run the command, if valid try: call(self.cmd) except: print ("Unable to run: %s" % self.cmd) class appButtons: def __init__(self,gui,app_index): #Add the buttons to window btn = tk.Button(gui, text=app_list[app_index][APP_NAME], width=30, command=self.startApp) btn.pack() self.app_cmd=app_list[app_index][APP_CMD] def startApp(self): print ("APP_CMD: %s" % self.app_cmd) 92 Chapter 3 runApplictionThread(self.app_cmd).start() root = tk.Tk() root.title("App Menu") prompt = ' Select an application ' label1 = tk.Label(root, text=prompt, width=len(prompt), bg='green') label1.pack() #Create menu buttons from app_list for index, app in enumerate(app_list): appButtons(root,index) #Run the tk window root.mainloop() #End The previous code produces the following application: The App Menu GUI How it works… We create the Tkinter window as we did before; however, instead of defining all the items separately, we create a special class for the application buttons. The class we create acts as a blueprint or specification of what we want the appButtons items to include. Each item will consist of a string value for app_cmd, a function called startApp(), and an __init__() function. The __init__() function is a special function (called a constructor) that is called when we create an appButtons item; it will allow us to create any setup that is required. In this case, the __init__() function allows us to create a new Tkinter button with the text to be set to an item in app_list and the command to be called in the startApp() function when the button is clicked. The self keyword is used so that the command called will be the one that is part of the item; this means that each button will call a locally defined function that has access to the local data of the item. 93 Using Python for Automation and Productivity We set the value of self.app_cmd to the command from app_list and make it ready for use by the startApp() function. We now create the startApp() function. If we run the application command here directly, the Tkinter window will freeze until the application we have opened is closed again. To avoid this, we can use the Python Threading module, which allows us to perform multiple actions at the same time. The runApplicationThread() class is created using the threading.Thread class as a template—this inherits all the features of the threading.Thread class in a new class. Just like our previous class, we provide an __init__() function for this as well. We first call the __init__() function of the inherited class to ensure it is set up correctly, and then we store the app_cmd value in self.cmd. After the runApplicationThread() function has been created and initialized, the start() function is called. This function is part of threading.Thread, which our class can use. When the start() function is called, it will create a separate application thread (that is, simulate running two things at the same time), allowing Tkinter to continue monitoring button clicks while executing the run() function within the class. Therefore, we can place the code in the run() function to run the required application (using call(self.cmd)). There's more… One aspect that makes Python particularly powerful is that it supports the programming techniques used in Object-Orientated Design (OOD). This is commonly used by modern programming languages to help translate the tasks we want our program to perform into meaningful constructs and structures in code. The principle of OOD lies in the fact that we think of most problems consisting of several objects (a GUI window, a button, and so on) that interact with each other to produce a desired result. In the previous section, we found that we can use classes to create standardized objects that can be reused multiple times. We created an appButton class, which generated an object with all the features of the class, including its own personal version of app_cmd that will be used by the startApp() function. Another object of the appButton type will have its own unrelated [app_cmd] data that its startApp() function will use. You can see that classes are useful to keep together a collection of related variables and functions in a single object, and the class will hold its own data in one place. Having multiple objects of the same type (class), each with their own functions and data inside them, results in better program structure. The traditional approach would be to keep all the information in one place and send each item back and forth for various functions to process; however, this may become cumbersome in large systems. 94 Chapter 3 The following diagram shows the organization of related functions and data: Related functions and data can be organized into classes and objects So far, we have used Python modules to separate parts of our programs into different files; this allows us to conceptually separate different parts of the program (an interface, encoder/decoder, or library of classes, such as Tkinter). Modules can provide code to control a particular bit of hardware, define an interface for the Internet, or provide a library of common functionality; however, its most important function is to control the interface (the collection of functions, variables, and classes that are available when the item is imported). A well implemented module should have a clear interface that is centered around how it is used, rather than how it is implemented. This allows you to create multiple modules that can be swapped and changed easily since they share the same interface. In our previous example, imagine how easy it would be to change the encryptdecrypt module for another one just by supporting encryptText(input_text,key). Complex functionality can be split into smaller, manageable blocks that can be reused in multiple applications. Python makes use of classes and modules all the time. Each time you import a library, such as sys or Tkinter, or convert a value using value.str() and iterate through a list using for...in, you can use them without worrying about the details. You don't have to use classes or modules in every bit of code you write, but they are useful tools to keep in your programmer's toolbox for times when they fit what you are doing. We will understand how classes and modules allow us to produce well-structured code that is easier to test and maintain by using them in the examples of this book. 95 Using Python for Automation and Productivity Displaying photo information in an application In this example, we shall create a utility class to handle photos that can be used by other applications (as a module) to access photo metadata and display preview images easily. Getting ready The following script makes use of Python Image Library (PIL); a compatible version for Python 3 is Pillow. Pillow has not been included in the Raspbian repository (used by apt-get); therefore, we will need to install Pillow using a Python Package Manager called PIP. To install packages for Python 3, we will use the Python 3 version of PIP (this requires 50 MB of available space). The following commands can be used to install PIP: sudo apt-get update sudo apt-get install python3-pip Before you use PIP, ensure that you have installed libjpeg-dev to allow Pillow to handle JPEG files. You can do this using the following command: sudo apt-get install libjpeg-dev Now you can install Pillow using the following PIP command: sudo pip-3.2 install pillow PIP also makes it easy to uninstall packages using uninstall instead of install. Finally, you can confirm that it has installed successfully by running python3: >>>import PIL >>>help(PIL) You should not get any errors and see lots of information about PIL and its uses (press Q to finish). Check the version installed as follows: >>PIL.PILLOW_VERSION You should see 2.7.0 (or similar). 96 Chapter 3 PIP can also be used with Python 2 by installing pip-2.x using the following command: sudo apt-get install python-pip Any packages installed using sudo pip install will be installed just for Python 2. How to do it… To display photo information in an application, create the following photohandler.py script: ##!/usr/bin/python3 #photohandler.py from PIL import Image from PIL import ExifTags import datetime import os #set module values previewsize=240,240 defaultimagepreview="./preview.ppm" filedate_to_use="Exif DateTime" #Define expected inputs ARG_IMAGEFILE=1 ARG_LENGTH=2 class Photo: def __init__(self,filename): """Class constructor""" self.filename=filename self.filevalid=False self.exifvalid=False img=self.initImage() if self.filevalid==True: self.initExif(img) self.initDates() def initImage(self): """opens the image and confirms if valid, returns Image""" try: img=Image.open(self.filename) self.filevalid=True except IOError: 97 Using Python for Automation and Productivity print ("Target image not found/valid %s" % (self.filename)) img=None self.filevalid=False return img def initExif(self,image): """gets any Exif data from the photo""" try: self.exif_info={ ExifTags.TAGS[x]:y for x,y in image._getexif().items() if x in ExifTags.TAGS } self.exifvalid=True except AttributeError: print ("Image has no Exif Tags") self.exifvalid=False def initDates(self): """determines the date the photo was taken""" #Gather all the times available into YYYY-MM-DD format self.filedates={} if self.exifvalid: #Get the date info from Exif info exif_ids=["DateTime","DateTimeOriginal", "DateTimeDigitized"] for id in exif_ids: dateraw=self.exif_info[id] self.filedates["Exif "+id]= dateraw[:10].replace(":","-") modtimeraw = os.path.getmtime(self.filename) self.filedates["File ModTime"]="%s" % datetime.datetime.fromtimestamp(modtimeraw).date() createtimeraw = os.path.getctime(self.filename) self.filedates["File CreateTime"]="%s" % datetime.datetime.fromtimestamp(createtimeraw).date() def getDate(self): """returns the date the image was taken""" try: date = self.filedates[filedate_to_use] except KeyError: 98 Chapter 3 print ("Exif Date not found") date = self.filedates["File ModTime"] return date def previewPhoto(self): """creates a thumbnail image suitable for tk to display""" imageview=self.initImage() imageview=imageview.convert('RGB') imageview.thumbnail(previewsize,Image.ANTIALIAS) imageview.save(defaultimagepreview,format='ppm') return defaultimagepreview The previous code defines our Photo class; it is of no use to us until we run it in the There's more… section and in the next example. How it works… We define a general class called Photo; it contains details about itself and provides functions to access Exchangeable Image File Format (EXIF) information and generate a preview image. In the __init__() function, we set values for our class variables and call self. initImage(), which will open the image using the Image() function from the PIL. We then call self.initExif() and self.initDates() and set a flag to indicate whether the file was valid or not. If not valid, the Image() function would raise an IOError exception. The initExif() function uses PIL to read the EXIF data from the img object, as shown in the following code snippet: self.exif_info={ ExifTags.TAGS[id]:y for id,y in image._getexif().items() if id in ExifTags.TAGS } The previous code is a series of compound statements that result in self.exif_info being populated with a dictionary of tag names and their related values. ExifTag.TAGS is a dictionary that contains a list of possible tag names linked with their IDs, as shown in the following code snippet: ExifTag.TAGS={ 4096: 'RelatedImageFileFormat', 513: 'JpegIFOffset', 514: 'JpegIFByteCount', 40963: 'ExifImageHeight', …etc…} 99 Using Python for Automation and Productivity The image._getexif() function returns a dictionary that contains all the values set by the camera of the image, each linked to their relevant IDs, as shown in the following code snippet: Image._getexif()={ 256: 3264, 257: 2448, 37378: (281, 100), 36867: '2016:09:28 22:38:08', …etc…} The for loop will go through each item in the image's EXIF value dictionary and check for its occurrence in the ExifTags.TAGS dictionary; the result will get stored in self.exif_info. The code for this is as follows: self.exif_info={ 'YResolution': (72, 1), 'ResolutionUnit': 2, 'ExposureMode': 0, 'Flash': 24, …etc…} Again, if there are no exceptions, we set a flag to indicate that the EXIF data is valid, or if there is no EXIF data, we raise an AttributeError exception. The initDates() function allows us to gather all the possible file dates and dates from the EXIF data so that we can select one of them as the date we wish to use for the file. For example, it allows us to rename all the images to a filename in the standard date format. We create a self.filedates dictionary that we populate with three dates extracted from the EXIF information. We then add the filesystem dates (created and modified) just in case no EXIF data is available. The os module allows us to use os.path.getctime() and os.path. getmtime() to obtain an epoch value of the file creation—it can also be the date and time when the file was moved—and file modification—when it was last written to (for example, it often refers to the date when the picture was taken). The epoch value is the number of seconds since January 1, 1970, but we can use datetime.datetime.fromtimestamp() to convert it into years, months, days, hours, and seconds. Adding date() simply limits it to years, months, and days. Now if the Photo class was to be used by another module, and we wished to know the date of the image that was taken, we could look at the self.dates dictionary and pick out a suitable date. However, this would require the programmer to know how the self. dates values are arranged, and if we later changed how they are stored, it would break their program. For this reason, it is recommended that we access data in a class through access functions so the implementation is independent of the interfaces (this process is known as encapsulation). We provide a function that returns a date when called; the programmer does not need to know that it could be one of the five available dates or even that they are stored as epoch values. Using a function, we can ensure that the interface will remain the same no matter how the data is stored or collected. 100 Chapter 3 Finally, the last function we want the Photo class to provide is previewPhoto(). This function provides a method to generate a small thumbnail image and save it as a Portable Pixmap Format (PPM) file. As we will discover in a moment, Tkinter allows us to place images on its Canvas widget, but unfortunately, it does not support JPGs (and only supports GIF or PPM) directly. Therefore, we simply save a small copy of the image we want to display in the PPM format—with the added caveat that the image pallet must be converted to RGB too—and then get Tkinter to load it onto the Canvas when required. To summarize, the Photo class we have created is as follows: Operations __init__(self,filename) Description initImage(self) This returns img, a PIL-type image object initExif(self,image) This extracts all the EXIF information, if any is present initDates(self) This creates a dictionary of all the dates available from the file and photo information getDate(self) This returns a string of the date when the photo was taken/created previewPhoto(self) This returns a string of the filename of the previewed thumbnail This is the object initialization function The properties and their respective descriptions are as follows: Properties self.filename Description self.filevalid This is set to True if the file is opened successfully self.exifvalid This is set to True if the photo contains EXIF information self.exif_info This contains the EXIF information from the photo self.filedates This contains a dictionary of the available dates from the file and photo information The filename of the photo To test the new class, we will create some test code to confirm that everything is working as we expect; see the following section. There's more… We previously created the Photo class. Now we can add some test code to our module to ensure that it functions as we expect. We can use the __name__ ="__main__" attribute as before to detect whether the module has been run directly or not. 101 Using Python for Automation and Productivity We can add the subsequent section of code at the end of the photohandler.py script to produce the following test application, which looks as follows: The Photo View Demo application Add the following code at the end of photohandler.py: #Module test code def dispPreview(aPhoto): """Create a test GUI""" import tkinter as TK #Define the app window app = TK.Tk() app.title("Photo View Demo") #Define TK objects # create an empty canvas object the same size as the image canvas = TK.Canvas(app, width=previewsize[0], height=previewsize[1]) canvas.grid(row=0,rowspan=2) # Add list box to display the photo data #(including xyscroll bars) photoInfo=TK.Variable() lbPhotoInfo=TK.Listbox(app,listvariable=photoInfo, height=18,width=45, font=("monospace",10)) yscroll=TK.Scrollbar(command=lbPhotoInfo.yview, orient=TK.VERTICAL) 102 Chapter 3 xscroll=TK.Scrollbar(command=lbPhotoInfo.xview, orient=TK.HORIZONTAL) lbPhotoInfo.configure(xscrollcommand=xscroll.set, yscrollcommand=yscroll.set) lbPhotoInfo.grid(row=0,column=1,sticky=TK.N+TK.S) yscroll.grid(row=0,column=2,sticky=TK.N+TK.S) xscroll.grid(row=1,column=1,sticky=TK.N+TK.E+TK.W) # Generate the preview image preview_filename = aPhoto.previewPhoto() photoImg = TK.PhotoImage(file=preview_filename) # anchor image to NW corner canvas.create_image(0,0, anchor=TK.NW, image=photoImg) # Populate infoList with dates and exif data infoList=[] for key,value in aPhoto.filedates.items(): infoList.append(key.ljust(25) + value) if aPhoto.exifvalid: for key,value in aPhoto.exif_info.items(): infoList.append(key.ljust(25) + str(value)) # Set listvariable with the infoList photoInfo.set(tuple(infoList)) app.mainloop() def main(): """called only when run directly, allowing module testing""" import sys #Check the arguments if len(sys.argv) == ARG_LENGTH: print ("Command: %s" %(sys.argv)) #Create an instance of the Photo class viewPhoto = Photo(sys.argv[ARG_IMAGEFILE]) #Test the module by running a GUI if viewPhoto.filevalid==True: dispPreview(viewPhoto) else: print ("Usage: photohandler.py imagefile") if __name__=='__main__': main() #End 103 Using Python for Automation and Productivity The previous test code will run the main() function, which takes the filename of a photo to use and create a new Photo object called viewPhoto. If viewPhoto is opened successfully, we will call dispPreview() to display the image and its details. The dispPreview() function creates four Tkinter widgets to be displayed: a Canvas to load the thumbnail image, a Listbox widget to display the photo information, and two scroll bars to control the Listbox. First, we create a Canvas widget the size of the thumbnail image (previewsize). Next, we create photoInfo, which will be our listvariable parameter linked to the Listbox widget. Since Tkinter doesn't provide a ListVar() function to create a suitable item, we use the generic type TK.Variable() and then ensure we convert it to a tuple type before setting the value. The Listbox widget gets added; we need to make sure that the listvariable parameter is set to photoInfo and also set the font to monospace. This will allow us to line up our data values using spaces, as monospace is a fixed width font, so each character takes up the same width as any other. We define the two scroll bars, linking them to the Listbox widget, by setting the Scrollbar command parameters for vertical and horizontal scroll bars to lbPhotoInfo.yview and lbPhotoInfo.xview. Then, we adjust the parameters of the Listbox using the following command: lbPhotoInfo.configure(xscrollcommand=xscroll.set, yscrollcommand=yscroll.set) The configure command allows us to add or change the widget's parameters after it has been created, in this case linking the two scroll bars so the Listbox widget can also control them if the user scrolls within the list. As before, we make use of the grid layout to ensure that the Listbox widget has the two scroll bars placed correctly next to it and the Canvas widget is to the left of the Listbox widget. We now use the Photo object to create the preview.ppm thumbnail file (using the aPhoto. previewPhoto() function) and create a TK.PhotoImage object that can then be added to the Canvas widget with the following command: canvas.create_image(0,0, anchor=TK.NW, image=photoImg) Finally, we use the date information that the Photo class gathers and the EXIF information (ensuring it is valid first) to populate the Listbox widget. We do this by converting each item into a list of strings that are spaced out using .ljust(25)—it adds a left justification to the name and pads it out to make the string 25 characters wide. Once we have the list, we convert it to a tuple type and set the listvariable (photoInfo) parameter. As always, we call app.mainloop() to start the monitoring for events to respond to. 104 Chapter 3 Organizing your photos automatically Now that we have a class that allows us to gather information about photos, we can apply this information to perform useful tasks. In this case, we will use the file information to automatically organize a folder full of photos into a subset of folders based on the dates the photos were taken on. The following screenshot shows the output of the script: The application will use the photo's information to sort pictures into folders by the date on which they were taken Getting ready You will need a selection of photos placed in a folder on the Raspberry Pi. Alternatively, you can insert a USB memory stick or a card reader with photos on it—they will be located in / mnt/. However, please make sure you test the scripts with a copy of your photos first, just in case there are any problems. How to do it… Create the following script in filehandler.py to automatically organize your photos: #!/usr/bin/python3 #filehandler.py import os 105 Using Python for Automation and Productivity import shutil import photohandler as PH from operator import itemgetter FOLDERSONLY=True DEBUG=True defaultpath="" NAME=0 DATE=1 class FileList: def __init__(self,folder): """Class constructor""" self.folder=folder self.listFileDates() def getPhotoNamedates(self): """returns the list of filenames and dates""" return self.photo_namedates def listFileDates(self): """Generate list of filenames and dates""" self.photo_namedates = list() if os.path.isdir(self.folder): for filename in os.listdir(self.folder): if filename.lower().endswith(".jpg"): aPhoto = PH.Photo(os.path.join(self.folder,filename)) if aPhoto.filevalid: if (DEBUG):print("NameDate: %s %s"% (filename,aPhoto.getDate())) self.photo_namedates.append((filename, aPhoto.getDate())) self.photo_namedates = sorted(self.photo_namedates, key=lambda date: date[DATE]) def genFolders(self): """function to generate folders""" for i,namedate in enumerate(self.getPhotoNamedates()): #Remove the - from the date format new_folder=namedate[DATE].replace("-","") newpath = os.path.join(self.folder,new_folder) #If path does not exist create folder if not os.path.exists(newpath): 106 Chapter 3 if (DEBUG):print ("New Path: %s" % newpath) os.makedirs(newpath) if (DEBUG):print ("Found file: %s move to %s" % (namedate[NAME],newpath)) src_file = os.path.join(self.folder,namedate[NAME]) dst_file = os.path.join(newpath,namedate[NAME]) try: if (DEBUG):print ("File moved %s to %s" % (src_file, dst_file)) if (FOLDERSONLY==False):shutil.move(src_file, dst_file) except IOError: print ("Skipped: File not found") def main(): """called only when run directly, allowing module testing""" import tkinter as TK from tkinter import filedialog app = TK.Tk() app.withdraw() dirname = TK.filedialog.askdirectory(parent=app, initialdir=defaultpath, title='Select your pictures folder') if dirname != "": ourFileList=FileList(dirname) ourFileList.genFolders() if __name__=="__main__": main() #End How it works… We shall make a class called FileList; it will make use of the Photo class to manage the photos within a specific folder. There are two main steps for this: we first need to find all the images within the folder, and then generate a list containing both the filename and the photo date. We will use this information to generate new subfolders and move the photos into these folders. When we create the FileList object, we will create the list using listFileDates(). We will then confirm that the folder provided is valid and use os.listdir to obtain the full list of files within the directory. We will check that each file is a .jpg file and obtain each photo's date (using the function defined in the Photo class). Next, we will add the filename and date as a tuple to the self.photo_namedates list. 107 Using Python for Automation and Productivity Finally, we will use the built-in sorted function to place all the files in order of their date. While we don't need to do this here, this function would make it easier to remove duplicate dates if we were to use this module elsewhere. The sorted function requires the list to be sorted, and in this case, we want to sort it by the date values. sorted(self.photo_namedates,key=lambda date: date[DATE]) We will substitute date[DATE] with lambda date: as the value to sort by. Once the FileList object has been initialized, we can use it by calling genFolders(). First, we convert the date text into a suitable format for our folders (YYYYMMDD), allowing our folders to be easily sorted in order of their date. Next, it will create the folders within the current directory if they don't already exist. Finally, it will move each of the files into the required subfolder. We end up with our FileList class that is ready to be tested: Operations __init__(self,folder) Description getPhotoNamedates(self) This returns a list of the filenames of the dates of the photos listFileDates(self) This creates a list of the filenames and dates of the photos in the folder genFolders(self) This creates new folders based on a photo's date and moves the files into them This is the object initialization function The properties are listed as follows: Properties self.folder Description self.photo_namedates This contains a list of the filenames and dates 108 The folder we are working with Chapter 3 The FileList class encapsulates all the functions and the relevant data together, keeping everything in one logical place: Tkinter filediaglog.askdirectory() is used to select the photo directory To test this, we use the Tkinter filedialog.askdirectory() widget to allow us to select a target directory of pictures. We use app.withdrawn() to hide the main Tkinter window since it isn't required this time. We just need to create a new FileList object and then call genFolders() to move all our photos to new locations! Two additional flags have been defined in this script that provide an extra control for testing. DEBUG allows us to enable or disable extra debugging messages by setting them to either True or False. Furthermore, from this, FOLDERSONLY when set to True only generates the folders and doesn't move the files (this is helpful for testing whether the new subfolders are correct). Once you have run the script, you can check if all the folders have been created correctly. Finally, change FOLDERSONLY to True, and your program will automatically move and organize your photos according to their dates the next time. It is recommended that you only run this on a copy of your photos, just in case you get an error. 109 4 Creating Games and Graphics In this chapter, we will cover the following topics: ff Using IDLE3 to debug your programs ff Drawing lines using a mouse on a Tkinter Canvas ff Creating a bat and ball game ff Creating an overhead scrolling game Introduction Games are often a great way to explore and extend your programming skills as they present an inherent motivating force to modify and improve your creation, add new features, and create new challenges. They are also great for sharing your ideas with others, even if they aren't interested in programming. This chapter focuses on using the Tkinter Canvas widget to create and display objects on screen for the user to interact with. Using these techniques, a wide variety of games and applications can be created that are limited only by your own creativity. We also take a quick look at using the debugger built into IDLE, a valuable tool for testing and developing your programs without the need to write extensive test code. The first example demonstrates how we can monitor and make use of the mouse to create objects and draw directly on the Canvas widget. Next, we create a bat and ball game, which shows how the positions of objects can be controlled and how interactions between them can be detected and responded to. Finally, we take things a little further and use Tkinter to place our own graphics onto the Canvas widget to create an overhead view treasure hunt game. 111 Creating Games and Graphics Using IDLE3 to debug your programs A key aspect of programming is being able to test and debug your code, and a useful tool to achieve this is a debugger. The IDLE editor (make sure you use IDLE3 to support the Python 3 code we use in this book) includes a basic debugger. It allows you to step through your code, observe the values of local and global variables, and set breakpoints. How to do it… To enable the debugger, start IDLE3 and select Debugger from the Debug menu; it will open up the following window (if you are currently running some code, you will need to stop it first): The IDLE3 debugger window Open up the code you want to test (via File | Open…) and try running it (F5). You will find that the code will not start, since the debugger has automatically stopped at the first line. The following screenshot shows the debugger has stopped on the first line of code in filehandler.py, which is line 3: import os: 112 Chapter 4 The IDLE3 debugger at the start of the code How it works… The control buttons shown in the following screenshot allow you to run and/or jump through the code: Debugger controls The functions of the control buttons are as follows: ff Go: This button will execute the code as normal. ff Step: This button will execute the line of code one step at a time and then stop again. If a function is called, it will enter that function and allow you to step through that too. ff Over: This button is like the Step command, but if there is a function call, it will execute the whole function and stop at the following line. ff Out: This button will keep executing the code until it has completed the function it is currently in, continuing until you come out of the function. ff Quit: This button ends the program immediately. 113 Creating Games and Graphics In addition to the previously mentioned controls, you can Set Breakpoint and Clear Breakpoint directly within the code. A breakpoint is a marker that you can insert in the code (by right-clicking on the editor window), which the debugger will always break on (stop at) when it is reached, as shown in the following screenshot: Set and clear breakpoints directly in your code The checkboxes (on the right-hand side of the control buttons) allow you to choose what information to display when you step through the code or when the debugger stops somewhere due to a breakpoint. Stack is shown in the main window, which is similar to what you would see if the program hit an unhandled exception. The Stack option shows all the function calls made to get to the current position in the code, right up to the line it has stopped at. The Source option highlights the line of code currently being executed and, in some cases, the code inside the imported modules too (if they are non-compiled libraries). You can also select whether to display Locals and/or Globals. By default, the Source and Globals options are usually disabled as they can make the process quite slow if there is a lot of data to display. Python uses the concept of local and global variables to define the scope (where and when the variables are valid). Global variables are defined at the top level of the file and are visible from any point in the code after it has been defined. However, in order to alter its value from anywhere other than the top level, Python requires you to use the global keyword first. Without the global keyword, you will create a local copy with the same name (the value of which will be lost when you exit the function). Local variables are defined when you create a variable within a function; once outside of the function, the variable is destroyed and is not visible anymore. 114 Chapter 4 Below Stack data are the Locals, in this case aPhoto, filename, and self. Then (if enabled), we have all the global values that are currently valid providing useful details about the status of the program (DATE = 1, DEBUG = True, FOLDERSONLY = True, and so on): The Stack, Locals, and Globals options within the debugger The debugger isn't particularly advanced, as it does not allow you to expand complex objects such as the photohandler.Photo object to see what data it contains. However, if required, you can adjust your code and assign the data you want to observe to some temporary variables during testing. It is worth learning how to use the debugger as it is a much easier way to track down particular problems and check whether or not things are functioning as you expect them to. 115 Creating Games and Graphics Drawing lines using a mouse on Tkinter Canvas The Tkinter Canvas widget provides an area to create and draw objects on. The following script demonstrates how to use mouse events to interact with Tkinter. By detecting the mouse clicks, we can use Tkinter to draw a line that follows the movement of the mouse: A simple drawing application using Tkinter Getting ready As before, we need to have Tkinter installed and either the Raspbian desktop running (startx from the command line) or an SSH session with X11 Forwarding and an X server running (see Chapter 1, Getting Started with a Raspberry Pi Computer). We will also need a mouse connected. How to do it… Create the following script, painting.py: #!/usr/bin/python3 #painting.py import tkinter as TK #Set defaults btn1pressed = False 116 Chapter 4 newline = True def main(): root = TK.Tk() the_canvas = TK.Canvas(root) the_canvas.pack() the_canvas.bind(" ", mousemove) the_canvas.bind(" ", mouse1press) the_canvas.bind(" ", mouse1release) root.mainloop() def mouse1press(event): global btn1pressed btn1pressed = True def mouse1release(event): global btn1pressed, newline btn1pressed = False newline = True def mousemove(event): if btn1pressed == True: global xorig, yorig, newline if newline == False: event.widget.create_line(xorig,yorig,event.x,event.y, smooth=TK.TRUE) newline = False xorig = event.x yorig = event.y if __name__ == "__main__": main() #End How it works… The Python code creates a Tkinter window that contains a Canvas object called the_canvas. We use the bind function here, which will bind a specific event that occurs on this widget (the_canvas) to a specific action or key press. In this case, we bind the function of the mouse plus the click and release of the first mouse button ( and ). Each of these events are then used to call the mouse1press(), mouse1release(), and mousemove() functions. 117 Creating Games and Graphics The logic here is as follows. We track the status of the mouse button using the mouse1press() and mouse1release() functions. If the mouse has been clicked, the mousemove() function will check to see whether we are drawing a new line (we set new coordinates for this) or continuing an old one (we draw a line from the previous coordinates to the coordinates of the current event that has triggered mousemove()). We just need to ensure that we reset to the newline command whenever the mouse button is released to reset the start position of the line. Creating a bat and ball game A classic bat and ball game can be created using the drawing tools of canvas and by detecting the collisions of the objects. The user will be able to control the green paddle using the left and right cursor keys to aim the ball at the bricks and hit them until they have all been destroyed. Control the bat to aim the ball at the bricks 118 Chapter 4 Getting ready This example requires graphical output, so you must have a screen and keyboard attached to the Raspberry Pi or use X11 Forwarding and X server if connected remotely from another computer. How to do it… Create the following script, bouncingball.py. 1. First, import the tkinter and time modules, and define constants for the game graphics: #!/usr/bin/python3 # bouncingball.py import tkinter as TK import time VERT,HOREZ=0,1 xTOP,yTOP = 0,1 xBTM,yBTM = 2,3 MAX_WIDTH,MAX_HEIGHT = 640,480 xSTART,ySTART = 100,200 BALL_SIZE=20 RUNNING=True 2. Next, create functions for closing the program, moving the paddle right and left, and for calculating the direction of the ball: def close(): global RUNNING RUNNING=False root.destroy() def move_right(event): if canv.coords(paddle)[xBTM]<(MAX_WIDTH-7): canv.move(paddle, 7, 0) def move_left(event): if canv.coords(paddle)[xTOP]>7: canv.move(paddle, -7, 0) def determineDir(ball,obj): global delta_x,delta_y if (ball[xTOP] == obj[xBTM]) or (ball[xBTM] == obj[xTOP]): 119 Creating Games and Graphics delta_x = -delta_x elif (ball[yTOP] == obj[yBTM]) or (ball[yBTM] == obj[yTOP]): delta_y = -delta_y 3. Set up the tkinter window and define the canvas: root = TK.Tk() root.title("Bouncing Ball") root.geometry('%sx%s+%s+%s' %(MAX_WIDTH, MAX_HEIGHT, 100, 100)) root.bind(' ', move_right) root.bind(' ', move_left) root.protocol('WM_DELETE_WINDOW', close) canv = TK.Canvas(root, highlightthickness=0) canv.pack(fill='both', expand=True) 4. Add the borders, ball, and paddle objects to the canvas: top = canv.create_line(0, 0, MAX_WIDTH, 0, fill='blue', tags=('top')) left = canv.create_line(0, 0, 0, MAX_HEIGHT, fill='blue', tags=('left')) right = canv.create_line(MAX_WIDTH, 0, MAX_WIDTH, MAX_HEIGHT, fill='blue', tags=('right')) bottom = canv.create_line(0, MAX_HEIGHT, MAX_WIDTH, MAX_HEIGHT, fill='blue', tags=('bottom')) ball = canv.create_rectangle(0, 0, BALL_SIZE, BALL_SIZE, outline='black', fill='black', tags=('ball')) paddle = canv.create_rectangle(100, MAX_HEIGHT - 30, 150, 470, outline='black', fill='green', tags=('rect')) 5. Draw all the bricks and set up the ball and paddle positions: brick=list() for i in range(0,16): for row in range(0,4): brick.append(canv.create_rectangle(i*40, row*20, ((i+1)*40)-2, ((row+1)*20)-2, outline='black', fill='red', tags=('rect'))) delta_x = delta_y = 1 xold,yold = xSTART,ySTART canv.move(ball, xold, yold) 120 Chapter 4 6. Create the main loop for the game to check for collisions and handle the movement of the paddle and ball: while RUNNING: objects = canv.find_overlapping(canv.coords(ball)[0], canv.coords(ball)[1], canv.coords(ball)[2], canv.coords(ball)[3]) #Only change the direction once (so will bounce off 1st # block even if 2 are hit) dir_changed=False for obj in objects: if (obj != ball): if dir_changed==False: determineDir(canv.coords(ball),canv.coords(obj)) dir_changed=True if (obj >= brick[0]) and (obj <= brick[len(brick)-1]): canv.delete(obj) if (obj == bottom): text = canv.create_text(300,100,text="YOU HAVE MISSED!") canv.coords(ball, (xSTART,ySTART, xSTART+BALL_SIZE,ySTART+BALL_SIZE)) delta_x = delta_y = 1 canv.update() time.sleep(3) canv.delete(text) new_x, new_y = delta_x, delta_y canv.move(ball, new_x, new_y) canv.update() time.sleep(0.005) #End How it works… We create a Tkinter application that is 640 x 480 pixels and bind the and cursor keys to the move_right() and move_left() functions. We use root. protocol('WM_DELETE_WINDOW', close) to detect when the window is closed so that we can cleanly exit the program (via close(), which sets RUNNING to False). 121 Creating Games and Graphics We then add a Canvas widget to the application that will hold all our objects. We create the following objects: top, left, right, and bottom. These make up our bounding sides for our game area. The canvas coordinates are 0,0 in the top-left corner and 640,480 in the bottom-right corner, so the start and end coordinates can be determined for each side (using canv.create_line(xStart, yStart, xEnd, yEnd)). The coordinates of the Canvas widget You can also add multiple tags to the objects; tags are often useful for defining specific actions or behaviors of objects. For instance, they allow for different types of event to occur when specific objects or bricks are hit. We see more uses of tags in the next example. Next, we define the ball and paddle objects, which are added using canv.create_ rectangle(). This requires two sets of coordinates that define the opposite corners of the Incorrect image, should be the following image with the 4x16 bricks on objects (in this case, the top-left and bottom-right corners). A tkinter rectangle is defined by the coordinates of the two corners. Finally, we can create the bricks! 122 Chapter 4 We want our bricks to be 40 x 20 pixels wide so we can fit 16 bricks across our game area of 640 pixels (in four rows). We can create a list of brick objects with their positions defined automatically, as shown in the following code: brick=list() for i in range(0,16): for row in range(0,4): brick.append(canv.create_rectangle(i*40, row*20, ((i+1)*40)-2, ((row+1)*20)-2, outline='black', fill='red', tags=('rect'))) A brick-like effect is provided by making the bricks slightly smaller (-2) to create a small gap. Four rows of 16 bricks are generated at the top of Canvas We now set the default settings before starting the main control loop. The movement of the ball will be governed by delta_x and delta_y, which are added to or subtracted from the ball's previous position in each cycle. Next, we set the starting position of the ball and use the canv.move() function to move the ball by that amount. The move() function will add 100 to the x and y coordinates of the ball object, which was originally created at position 0,0. Now that everything is set up, the main loop can run; this will check that the ball has not hit anything (using the canv.find_overlapping() function), make any adjustments to the delta_x or delta_y values, and then apply them to move the ball to the next location. The sign of delta_x and delta_y determines the direction of the ball. Positive values will make the ball travel diagonally downwards and towards the right, while -delta_x will make it travel towards the left, either downwards or upwards depending on whether delta_y is positive or negative. After the ball has been moved, we use canv.update() to redraw any changes made to the display, and time.sleep() allows a small delay before checking and moving the ball again. 123 Creating Games and Graphics Object collisions are detected using the canv.find_overlapping() function. This returns a list of canvas objects that are found to be overlapping the bounds of a rectangle defined by the supplied coordinates. For example, in the case of the square ball, are any of the coordinates of the canvas objects within the space the ball is occupying? The objects are checked to detect if they overlap each other If the ball is found to be overlapping another object, such as the walls, the paddle, or one or more of the bricks, we need to determine which direction the ball should next travel in. Since we are using the coordinates of the ball as the area within which to check, the ball will always be listed so that we ignore them when we check the list of objects. We use the dir_changed flag to ensure that if we hit two bricks at the same time, we do not change direction twice before we move the ball. Otherwise, this would cause the ball to continue moving in the same direction even though it has collided with the bricks. So if the ball is overlapping something else, we can call determineDir() with the coordinates of the ball and the object to work out what the new direction should be. When the ball collides with something, we want the ball to bounce off it; fortunately, this is easy to simulate as we just need to change the sign of either delta_x or delta_y depending on whether we have hit something on the sides or the top/bottom. If the ball hits the bottom of another object, it means we were travelling upwards and should now travel downwards. However, we will continue to travel in the same direction on the x axis (be it left or right or just up). This can be seen from the following code: if (ball[xTOP] == obj[xBTM]) or (ball[xBTM] == obj[xTOP]): delta_x = -delta_x 124 Chapter 4 The determineDir() function looks at the coordinates of the ball and the object, and looks for a match between either the left and right x coordinates or the top and bottom y coordinates. This is enough to say whether the collision is on the sides or top/bottom, and we can set the delta_x or delta_y signs accordingly, as can be seen in the following code: if (obj >= brick[0]) and (obj <= brick[-1]): canv.delete(obj) Next, we can determine if we have hit a brick by checking whether the overlapping object ID is between the first and last ID bricks. If it was a brick, we can remove it using canv.delete(). Python allows the index values to wrap around rather than access the invalid memory, so an index value of -1 will provide us with the last item in the list. We use this to reference the last brick as brick [-1]. We also check to see whether the object being overlapped is the bottom line (in which case, the player has missed the ball with the paddle), so a short message is displayed briefly. We reset the position of the ball and delta_x/delta_y values. The canv.update() function ensures that the display is refreshed with the message before it is deleted (3 seconds later). Finally, the ball is moved by the delta_x/delta_y distance and the display is updated. A small delay is added here to reduce the rate of updates and the CPU time used. Otherwise, you will find that your Raspberry Pi will become unresponsive if it is spending 100 percent of its effort running the program. When the user presses the cursor keys, the move_right() and move_left() functions are called. They check the position of the paddle object, and if the paddle is not at the edge, the paddle will be moved accordingly. If the ball hits the paddle, the collision detection will ensure that the ball bounces off, just as if it has hit one of the bricks. You can extend this game further by adding a score for each block destroyed, allowing the player a finite number of lives that are lost when they miss the ball, and even writing some code to read in new brick layouts. 125 Creating Games and Graphics Creating an overhead scrolling game By using objects and images in our programs, we can create many types of 2D graphical games. In this recipe, we will create a treasure hunt game where the player is trying to find buried treasure (by pressing Enter to dig for it). Each time the treasure has not been found, the player is given a clue to how far away the treasure is; they can then use the cursor keys to move around and search until they find it. Dig for treasure in your own overhead scrolling game! Although this is a basic concept for a game, it could easily be extended to include multiple layouts, traps, and enemies to avoid, perhaps even additional tools or puzzles to solve. With a few adjustments to the graphics, the character could be exploring a dungeon, a spaceship, or hopping through the clouds collecting rainbows! 126 Chapter 4 Getting ready The following example uses a number of images; these are available as part of the book's resources. You will need to place the nine images in the same directory as the Python script. The required image files can be seen in the code bundle of this chapter. How to do it… Create the following script, scroller.py: 1. Begin by importing the required libraries and parameters: #!/usr/bin/python3 # scroller.py import tkinter as TK import time import math from random import randint STEP=7 xVAL,yVAL=0,1 MAX_WIDTH,MAX_HEIGHT=640,480 SPACE_WIDTH=MAX_WIDTH*2 SPACE_HEIGHT=MAX_HEIGHT*2 LEFT,UP,RIGHT,DOWN=0,1,2,3 SPACE_LIMITS=[0,0,SPACE_WIDTH-MAX_WIDTH, SPACE_HEIGHT-MAX_HEIGHT] DIS_LIMITS=[STEP,STEP,MAX_WIDTH-STEP,MAX_HEIGHT-STEP] BGN_IMG="bg.gif" PLAYER_IMG=["playerL.gif","playerU.gif", "playerR.gif","playerD.gif"] WALL_IMG=["wallH.gif","wallV.gif"] GOLD_IMG="gold.gif" MARK_IMG="mark.gif" newGame=False checks=list() 2. Provide functions to handle the movement of the player: def move_right(event): movePlayer(RIGHT,STEP) def move_left(event): movePlayer(LEFT,-STEP) def move_up(event): movePlayer(UP,-STEP) 127 Creating Games and Graphics def move_down(event): movePlayer(DOWN,STEP) def foundWall(facing,move): hitWall=False olCoords=[canv.coords(player)[xVAL], canv.coords(player)[yVAL], canv.coords(player)[xVAL]+PLAYER_SIZE[xVAL], canv.coords(player)[yVAL]+PLAYER_SIZE[yVAL]] olCoords[facing]+=move objects = canv.find_overlapping(olCoords[0],olCoords[1], olCoords[2],olCoords[3]) for obj in objects: objTags = canv.gettags(obj) for tag in objTags: if tag == "wall": hitWall=True return hitWall def moveBackgnd(movement): global bg_offset bg_offset[xVAL]+=movement[xVAL] bg_offset[yVAL]+=movement[yVAL] for obj in canv.find_withtag("bg"): canv.move(obj, -movement[xVAL], -movement[yVAL]) def makeMove(facing,move): if facing == RIGHT or facing == LEFT: movement=[move,0] #RIGHT/LEFT bgOffset=bg_offset[xVAL] playerPos=canv.coords(player)[xVAL] else: movement=[0,move] #UP/DOWN bgOffset=bg_offset[yVAL] playerPos=canv.coords(player)[yVAL] #Check Bottom/Right Corner if facing == RIGHT or facing == DOWN: if (playerPos+PLAYER_SIZE[xVAL]) < DIS_LIMITS[facing]: canv.move(player, movement[xVAL], movement[yVAL]) elif bgOffset < SPACE_LIMITS[facing]: moveBackgnd(movement) else: #Check Top/Left Corner if (playerPos) > DIS_LIMITS[facing]: 128 Chapter 4 canv.move(player, movement[xVAL], movement[yVAL]) elif bgOffset > SPACE_LIMITS[facing]: moveBackgnd(movement) def movePlayer(facing,move): hitWall=foundWall(facing,move) if hitWall==False: makeMove(facing,move) canv.itemconfig(player,image=playImg[facing]) 3. Add functions to check how far the player is from the hidden gold: def check(event): global checks,newGame,text if newGame: for chk in checks: canv.delete(chk) del checks[:] canv.delete(gold,text) newGame=False hideGold() else: checks.append( canv.create_image(canv.coords(player)[xVAL], canv.coords(player)[yVAL], anchor=TK.NW, image=checkImg, tags=('check','bg'))) distance=measureTo(checks[-1],gold) if(distance<=0): canv.itemconfig(gold,state='normal') canv.itemconfig(check,state='hidden') text = canv.create_text(300,100,fill="white", text=("You have found the gold in"+ " %d tries!"%len(checks))) newGame=True else: text = canv.create_text(300,100,fill="white", text=("You are %d steps away!"%distance)) canv.update() time.sleep(1) canv.delete(text) def measureTo(objectA,objectB): deltaX=canv.coords(objectA)[xVAL]-\ 129 Creating Games and Graphics canv.coords(objectB)[xVAL] deltaY=canv.coords(objectA)[yVAL]-\ canv.coords(objectB)[yVAL] w_sq=abs(deltaX)**2 h_sq=abs(deltaY)**2 hypot=math.sqrt(w_sq+h_sq) return round((hypot/5)-20,-1) 4. Add functions to help find a location to hide the gold in: def hideGold(): global gold goldPos=findLocationForGold() gold=canv.create_image(goldPos[xVAL], goldPos[yVAL], anchor=TK.NW, image=goldImg, tags=('gold','bg'), state='hidden') def findLocationForGold(): placeGold=False while(placeGold==False): goldPos=[randint(0-bg_offset[xVAL], SPACE_WIDTH-GOLD_SIZE[xVAL]-bg_offset[xVAL]), randint(0-bg_offset[yVAL], SPACE_HEIGHT-GOLD_SIZE[yVAL]-bg_offset[yVAL])] objects = canv.find_overlapping(goldPos[xVAL], goldPos[yVAL], goldPos[xVAL]+GOLD_SIZE[xVAL], goldPos[yVAL]+GOLD_SIZE[yVAL]) findNewPlace=False for obj in objects: objTags = canv.gettags(obj) for tag in objTags: if (tag == "wall") or (tag == "player"): findNewPlace=True if findNewPlace == False: placeGold=True return goldPos 5. Create the Tkinter application window and bind the keyboard events: root = TK.Tk() root.title("Overhead Game") root.geometry('%sx%s+%s+%s' %(MAX_WIDTH, MAX_HEIGHT, 100, 100)) 130 Chapter 4 root.resizable(width=TK.FALSE, height=TK.FALSE) root.bind(' ', move_right) root.bind(' ', move_left) root.bind(' ', move_up) root.bind(' ', move_down) root.bind(' ', check) canv = TK.Canvas(root, highlightthickness=0) canv.place(x=0,y=0,width=SPACE_WIDTH,height=SPACE_HEIGHT) 6. Initialize all the game objects (the background tiles, the player, the walls, and the gold): #Create background tiles bgnImg = TK.PhotoImage(file=BGN_IMG) BGN_SIZE = bgnImg.width(),bgnImg.height() background=list() COLS=int(SPACE_WIDTH/BGN_SIZE[xVAL])+1 ROWS=int(SPACE_HEIGHT/BGN_SIZE[yVAL])+1 for col in range(0,COLS): for row in range(0,ROWS): background.append(canv.create_image(col*BGN_SIZE[xVAL], row*BGN_SIZE[yVAL], anchor=TK.NW, image=bgnImg, tags=('background','bg'))) bg_offset=[0,0] #Create player playImg=list() for img in PLAYER_IMG: playImg.append(TK.PhotoImage(file=img)) #Assume images are all same size/shape PLAYER_SIZE=playImg[RIGHT].width(),playImg[RIGHT].height() player = canv.create_image(100,100, anchor=TK.NW, image=playImg[RIGHT], tags=('player')) #Create walls wallImg=[TK.PhotoImage(file=WALL_IMG[0]), TK.PhotoImage(file=WALL_IMG[1])] WALL_SIZE=[wallImg[0].width(),wallImg[0].height()] wallPosH=[(0,WALL_SIZE[xVAL]*1.5), (WALL_SIZE[xVAL],WALL_SIZE[xVAL]*1.5), (SPACE_WIDTH-WALL_SIZE[xVAL],WALL_SIZE[xVAL]*1.5), (WALL_SIZE[xVAL],SPACE_HEIGHT-WALL_SIZE[yVAL])] 131 Creating Games and Graphics wallPosV=[(WALL_SIZE[xVAL],0),(WALL_SIZE[xVAL]*3,0)] wallPos=[wallPosH,wallPosV] wall=list() for i,img in enumerate(WALL_IMG): for item in wallPos[i]: wall.append(canv.create_image(item[xVAL],item[yVAL], anchor=TK.NW, image=wallImg[i], tags=('wall','bg'))) #Place gold goldImg = TK.PhotoImage(file=GOLD_IMG) GOLD_SIZE=[goldImg.width(),goldImg.height()] hideGold() #Check mark checkImg = TK.PhotoImage(file=MARK_IMG) 7. Finally, start the mainloop() command to allow Tkinter to monitor for events: #Wait for actions from user root.mainloop() #End How it works… As before, we create a new Tkinter application that contains a Canvas widget so that we can add all of the game objects. We ensure that we bind the right, left, up, down and Enter keys, which will be our controls in the game. First, we place our background image (bg.gif) onto the Canvas widget. We calculate the number of images we can fit along the length and width to tile the whole canvas space, and locate them using suitable coordinates. Next, we create the player image (by creating playImg, a list of Tkinter image objects for each direction the player can turn in) and place it on the canvas. We now create the walls, the positions of which are defined by the wallPosH and wallPosV lists. These could be defined using the exact coordinates, perhaps even read from a file to provide an easy method to load different layouts for levels if required. By iterating through the lists, the horizontal and vertical wall images are put in position on the canvas. 132 Chapter 4 To complete the layout, we just need to hide the gold somewhere. Using the hideGold() function, we randomly determine a suitable place to locate the gold. Within findLocationForGold(), we use randint(0,value) to create a pseudo-random number (it is not totally random but good enough for this use) between 0 and value. In our case, the value we want is between 0 and the edge of our canvas space minus the size of the gold image and any bg_offset that has been applied to the canvas. This ensures it is not beyond the edge of the screen. We then check the potential location using the find_ overlapping() function to see whether any objects with wall or player tags are in the way. If so, we pick a new location. Otherwise, we place the gold on the canvas but with the state="hidden" value, which will hide it from view. We then create checkImg (a Tkinter image) and use it while checking for gold to mark the area we have checked. Finally, we just wait for the user to press one of the keys. The character will move around the screen whenever one of the cursor keys is pressed. The player's movement is determined by the movePlayer() function; it will first check whether the player is trying to move into a wall, then determine (within the makeMove() function) if the player is at the edge of the display or canvas space. Every time a cursor key is pressed, we use the logic shown in the diagram to determine what to do 133 Creating Games and Graphics The foundWall() function works out whether the player will hit a wall by checking for any objects with wall tags within the area being covered by the player image, plus a little extra for the area that the player will be moving to next. The following diagram shows how the olCoords coordinates are determined: The coordinates to check for objects that overlap (olCoords) are calculated The makeMove() function checks if the player will be moving to the edge of the display (as defined by DIS_LIMITS) and whether they are at the edge of the canvas space (as defined by SPACE_LIMITS). Within the display, the player can be moved in the direction of the cursor, or all the objects tagged with bg within the canvas space are moved in the opposite direction, simulating scrolling behind the player. This is done by the moveBackground() function. When the player presses Enter, we'll want to check for gold in the current location. Using the measureTo() function, the position of the player and the gold are compared (the distance between the x and y coordinates of each is calculated). The distance between the player and the gold is calculated 134 Chapter 4 The result is scaled to provide a rough indication of how far away the player is from the gold. If the distance is greater than zero, we display how far away the player is from the gold and leave a cross to show where we have checked. If the player has found the gold, we display a message saying so and set newGame to True. The next time the player presses Enter, the places marked with a cross are removed and the gold is relocated somewhere new. With the gold hidden again, the player is ready to start again! 135 5 Creating 3D Graphics In this chapter, we will cover the following topics: ff Starting with 3D coordinates and vertices ff Creating and importing 3D models ff Creating a 3D world to roam in ff Building 3D maps and mazes Introduction The chip at the heart of the original Raspberry Pi (a Broadcom BCM2835 processor) was originally designed to be a Graphical Processing Unit (GPU) for mobile and embedded applications. The ARM core that drives most of the Raspberry Pi's functionality was added because some extra space was available on the chip; this enabled this powerful GPU to be used as a System-On-Chip (SoC) solution. As you can imagine, if that original ARM core (ARM1176JZF-S, which is the ARMv6 architecture) consisted of only a small part of the chip on the Raspberry Pi, you would be right in thinking that the GPU must perform rather well. The processor at the heart of the Raspberry Pi 3 has been upgraded (to a Broadcom BCM2837 processor); it now contains four ARM cores (Cortex A53 ARMv8A), each of which are more powerful than the original ARMv6. Coupled with the same GPU from the previous generation, the Raspberry Pi 3 is far better equipped to perform the calculations required to build 3D environments. However, although the Raspberry Pi 3 will load the examples faster, once the 3D models are generated, both versions of the chip perform just as well. 137 Creating 3D Graphics The VideoCore IV GPU consists of 48 purpose-built processors, with some providing support for 1080p high-definition encoding and decoding of video, while others support OpenGL ES 2.0, which provides fast calculations for 3D graphics. It has been said that its graphics processing power is equivalent to that of an Apple iPhone 4s and also the original Microsoft Xbox. This is even more apparent if you run Quake 3 or OpenArena on the Raspberry Pi (go to http://www.raspberrypi.org/openarena-for-raspberry-pi for details). In this chapter, I hope to show you that while you can achieve a lot by performing operations using the ARM side of the Raspberry Pi, if you venture into the side where the GPU is hidden, you may see that there is even more to this little computer than first appears. The Pi3D library created by the Pi3D team (Patrick Gaunt, Tom Swirly, Tim Skillman, and others) provides a way to put the GPU to work by creating 3D graphics. The Pi3D wiki and documentation pages can be found at the following link: http://pi3d.github.io/html/index.html The support/development group can be found at the following link: https://groups.google.com/forum/#!forum/pi3d The library contains many features, so it will not be possible to cover everything that is available in the following examples. It is recommended that you also take some time to try out the Pi3D demos. To discover more options for the creation and handling of the 3D graphics, you can have a look through some of the Python modules, which make up the library itself (described in the documentation or the code on GitHub at https://github.com/pi3d/ pi3d.github.com). It is hoped that this chapter will introduce you to enough concepts to illustrate some of the raw potential available to you. Starting with 3D coordinates and vertices The world around us is three-dimensional, so in order to simulate parts of the world, we can create a 3D representation and display it on our 2D screen. The Raspberry Pi enables us to simulate a 3D space, place 3D objects within it, and observe them from a selected viewpoint. We will use the GPU to produce a representation of the 3D view as a 2D image to display it on the screen. The following example will show how we can use Pi3D (an OpenGL ES library for the Raspberry Pi) to place a single 3D object and display it within the 3D space. We will then allow the mouse to rotate the view around the object. 138 Chapter 5 Getting ready The Raspberry Pi must be directly connected to a display, either via the HDMI or an analog video output. The 3D graphics rendered by the GPU will only be displayed on a local display, even if you are connecting to the Raspberry Pi remotely over a network. You will also need to use a locally connected mouse for control (however, keyboard control does work via a SSH connection). The first time we use Pi3D, we will need to download and install it with the following steps: 1. The Pi3D library uses Pillow, a version of the Python Imaging Library that is compatible with Python 3, to import graphics used in models (such as textures and backgrounds). The installation of Pillow has been covered in the Getting ready section of Chapter 3, Using Python for Automation and Productivity. The commands for the installation are shown in the following code (if you've installed them before, it will skip them and continue): sudo apt-get update sudo apt-get install python3-pip sudo apt-get install libjpeg-dev sudo pip-3.2 install pillow 139 Creating 3D Graphics 2. We can now use PIP to install Pi3D using the following command: sudo pip-3.2 install pi3d The Pi3D team is continuously developing and improving the library; if you are experiencing problems, it may mean that a new release is not compatible with the previous ones. You can also check in the Appendix, Hardware and Software List, to confirm which version of Pi3D you have and, if required, install the same version listed. Alternatively, contact the Pi3D team on the Google group; they will be happy to help! Obtain Pi3D demos from the GitHub site, as shown in the following command lines. You will need around 90 MB of free space to download and extract the files: cd ~ wget https://github.com/pi3d/pi3d_demos/archive/ master.zip unzip master.zip rm master.zip You will find that the demos have been unpacked to pi3d_demos-master. By default, the demos are expected to be located at home/pi/pi3d; therefore, we will rename this directory pi3d, as shown in the following command: mv pi3d_demos-master pi3d 3. Finally, check the Raspberry Pi memory split. Run raspi-config (sudo raspiconfig) and ensure that your memory split is set to 128. (You should only need to do this if you have changed it in the past, as 128 MB is the default.) This ensures that you have plenty of RAM allocated for the GPU, so it will be able to handle lots of 3D objects if required. 4. Test if everything is working properly. You should now be able to run any of the scripts in the pi3d_demos-master directory. See the Pi3D wiki pages for details of how they function (http://pi3d.github.io/html/ReadMe.html). To get the best performance, it is recommended that the scripts are run from the command prompt (without loading the desktop): cd pi3d python3 Raspberry_Rain.py 140 Chapter 5 Many of the demos require mouse and keyboard control. Although it would be perfectly reasonable to use the methods from Chapter 4, Creating Games and Graphics, for mouse and keyboard input using Tkinter, many of the demos in the Pi3D library use pi3d.Keyboard and pi3d.Mouse objects to provide additional support for joysticks and gamepads. The pi3d.Keyboard object also supports keyboard control via SSH (see the Connecting remotely to the Raspberry Pi over the network using SSH (and X11 Forwarding) section of Chapter 1, Getting Started with a Raspberry Pi Computer). Configure the setup for your own scripts. Since we will use some of the textures and models from the demos, it is recommended that you create your scripts within the pi3d directory. If you have a username that's different from the default Pi account, you will need to adjust /pi3d/demo. py. Replace the USERNAME part with your own username by editing the file: nano ~/pi3d/demo.py import sys sys.path.insert(1, '/home/USERNAME/pi3d') If you want to relocate your files somewhere else, ensure that you add a copy of demo.py in the folder with the correct path to any resource files you require. How to do it… Create the following 3dObject.py script: #!/usr/bin/python3 """ Create a 3D space with a Tetrahedron inside and rotate the view around using the mouse. """ from math import sin, cos, radians import demo import pi3d KEY = {'ESC':27,'NONE':-1} DISPLAY = pi3d.Display.create(x=50, y=50) #capture mouse and key presses mykeys = pi3d.Keyboard() mymouse = pi3d.Mouse(restrict = False) mymouse.start() def main(): 141 Creating 3D Graphics CAMERA = pi3d.Camera.instance() tex = pi3d.Texture("textures/stripwood.jpg") flatsh = pi3d.Shader("uv_flat") #Define the coordinates for our shape (x,y,z) A = (-1.0,-1.0,-1.0) B = (1.0,-1.0,1.0) C = (-1.0,-1.0,1.0) D = (-1.0,1.0,1.0) ids = ["A","B","C","D"] coords = [A,B,C,D] myTetra = pi3d.Tetrahedron(x=0.0, y=0.0, z=0.0, corners=(A,B,C,D)) myTetra.set_draw_details(flatsh,[tex]) # Load ttf font and set the font to black arialFont = pi3d.Font("fonts/FreeMonoBoldOblique.ttf", "#000000") mystring = [] #Create string objects to show the coordinates for i,pos in enumerate(coords): mystring.append(pi3d.String(font=arialFont, string=ids[i]+str(pos), x=pos[0], y=pos[1],z=pos[2])) mystring.append(pi3d.String(font=arialFont, string=ids[i]+str(pos), x=pos[0], y=pos[1],z=pos[2], ry=180)) for string in mystring: string.set_shader(flatsh) camRad = 4.0 # radius of camera position rot = 0.0 # rotation of camera tilt = 0.0 # tilt of camera k = KEY['NONE'] omx, omy = mymouse.position() # main display loop while DISPLAY.loop_running() and not k == KEY['ESC']: k = mykeys.read() mx, my = mymouse.position() rot -= (mx-omx)*0.8 tilt += (my-omy)*0.8 omx = mx 142 Chapter 5 omy = my CAMERA.reset() CAMERA.rotate(-tilt, rot, 0) CAMERA.position((camRad * sin(radians(rot)) * cos(radians(tilt)), camRad * sin(radians(tilt)), -camRad * cos(radians(rot)) * cos(radians(tilt)))) #Draw the Tetrahedron myTetra.draw() for string in mystring: string.draw() try: main() finally: mykeys.close() mymouse.stop() DISPLAY.destroy() print("Closed Everything. END") #End To run the script, use python3 3dObject.py. How it works… We import the math modules (for angle calculations—used to control the view based on mouse movements). We also import the demo module, which just provides the path to the shaders and textures in this example. We start by defining some key elements that will be used by Pi3D to generate and display our object. The space in which we shall place our object is the pi3d.Display object; this defines the size of the space and initializes the screen to generate and display OpenGL ES graphics. Next, we define a pi3d.Camera object, which will allow us to define how we view the object within our space. To render our object, we define a texture to be applied to the surface and a shader that will apply the texture to the object. The shader is used to apply all the effects and lighting to the object, and it is coded to use the GPU's OpenGL ES core instead of the ARM processor. 143 Creating 3D Graphics We define the keyboard and mouse object using pi3d.keyboard() and pi3d.mouse() so that we can respond to the keyboard and mouse input. The restrict flag of the mouse object allows the absolute mouse position to continue past the screen limits (so we can continuously rotate our 3D object). The main loop, when running, will check if the Esc key is pressed and then close everything down (including calling DISPLAY.destroy() to release the screen). We use the try: finally: method to ensure that the display is closed correctly even if there is an exception within main(). The mouse movement is collected in the main display loop using mymouse.position(), which returns the x and y coordinates. The difference in the x and y movement is used to rotate around the object. The mouse movements determine the position and angle of the camera. Any adjustment to the forward/backward position of the mouse is used to move it over or under the object and change the angle of the camera (using tilt) so it remains pointing at the object. Similarly, any sideways movement will move the camera around the object using the CAMERA.reset() function. This ensures that the display updates the camera view with the new position, CAMERA.rotate(), to change the angle and uses CAMERA.position() to move the camera to a position around the object, camRad units away from its center. We will draw a three-dimensional object called a tetrahedron, a shape made up of four triangles to form a pyramid with a triangular base. The four corners of the shape (three around the base and one at the top) will be defined by the three-dimensional coordinates A, B, C, and D, as shown in the following figure: The tetrahedron placed within the X, Y, and Z axes 144 Chapter 5 The pi3d.Tetrahedron object is defined by specifying coordinates to position it in the space and then specify the corners that will be joined to form the four triangles that make up the shape. Using set_draw_details(flatsh,[text]), we apply the shader(s) we wish to use and the texture(s) for the object. In our example, we are just using a single texture, but some shaders can use several textures for complex effects. To help highlight where the coordinates are, we will add some pi3d.String objects by setting the string text to specify the ID and coordinates next to them and placing it at the required location. We will create two string objects for each location, one facing forward and another facing backwards (ry=180 rotates the object by 180 degrees on the y axis). The pi3d.String objects are single-sided, so if we only had one side facing forward, it wouldn't be visible from behind when the view was rotated and would just disappear (plus, if it was visible, the text would be backwards anyway). Again, we use the flatsh shader to render it using the set_shader() string object. All that is left to do now is to draw our tetrahedron and the string objects while checking for any keyboard events. Each time the while loop completes, DISPLAY.loop_running() is called, which will update the display with any adjustments to the camera as required. There's more… In addition to introducing how to draw a basic object within the 3D space, the preceding example makes use of the following four key elements used in 3D graphics programming. Camera The camera represents our view in the 3D space; one way to explore and see more of the space is by moving the camera. The Camera class is defined as follows: pi3d.Camera.Camera(at=(0, 0, 0), eye=(0, 0, -0.1), lens=None, is_3d=True, scale=1.0) The camera is defined by providing two locations, one to look at (usually the object we wish to see—defined by at) and another to look from (the object's position—defined by eye). Other features of the camera, such as its field of view (lens) and so on, can be adjusted or used with the default settings. If we didn't define a camera in our display, a default one will be created that points at the origin (the center of the display, that is, 0,0,0), positioned slightly in front of it (0,0,-0.1). See the Pi3D documentation regarding the camera module for more details. 145 Creating 3D Graphics Shaders Shaders are very useful as they allow a lot of the complex work required to apply textures and lighting to an object by offloading the task to the more powerful GPU in the Raspberry Pi. The Shader class is defined as follows: class pi3d.Shader.Shader(shfile=None, vshader_source=None, fshader_source=None) This allows you to specify a shader file (shfile) and specific vertex and fragment shaders (if required) within the file. There are several shaders included in the Pi3D library, some of which allow multiple textures to be used for reflections, close-up details, and transparency effects. The implementation of the shader will determine how the lights and textures are applied to the object (and in some cases, such as uv_flat, the shader will ignore any lighting effects). The shader files are listed in the pi3d\shaders directory. Try experimenting with different shaders, such as mat_reflect, which will ignore the textures/fonts but still apply the lighting effects; or uv_toon, which will apply a cartoon effect to the texture. Each shader consists of two files, vs (vertex shader) and fs (fragment shader), written in C-like code. They work together to apply the effects to the object as desired. The vertex shader is responsible for mapping the 3D location of the vertices to the 2D display. The fragment shader (or sometimes called the pixel shader) is responsible for applying lighting and texture effects to the pixels themselves. The construction and operation of these shaders is well beyond the scope of this chapter, but there are several example shaders that you can compare, change, and experiment with within the pi3d\shaders directory. Lights Lighting is very important in a 3D world; it could range from simple general lighting (as used in our example) to multiple lights angled from different directions providing different strengths and colors. How lights interact with objects and the effects they produce will be determined by the textures and shaders used to render them. 146 Chapter 5 Lights are defined by their direction, their color and brightness, and also by an ambient light to define the background (non-directional) light. The Light class is defined as follows: class pi3d.Light (lightpos=(10, -10, 20), lightcol=(1.0, 1.0, 1.0), lightamb=(0.1, 0.1, 0.2)) By default, the display will define a light that has the following properties: ff lightpos=(10, -10, 20): This is a light that shines from the front of the space (near the top-left side) down towards the back of the space (towards the right). ff lightcol=(1.0, 1.0, 1.0): This is a bright, white, directional light (the direction is defined in the preceding dimension, and it is the color defined by the RGB values 1.0, 1.0, 1.0). ff lightamb=(0.1, 0.1, 0.2): This is overall a dull, slightly bluish light. Textures Textures are able to add realism to an object by allowing fine detail to be applied to the object's surface; this could be an image of bricks for a wall or a person's face to be displayed on the character. When a texture is used by the shader, it can often be re-scaled and reflection can be added to it; some shaders even allow you to apply surface detail. We can apply multiple textures to an object to combine them and produce different effects; it will be up to the shader to determine how they are applied. Creating and importing 3D models Creating complex shapes directly from code can often be cumbersome and time consuming. Fortunately, it is possible to import prebuilt models into your 3D space. 147 Creating 3D Graphics It is even possible to use graphical 3D modeling programs to generate models and then export them as a suitable format for you to use. This example produces a Newell Teapot in the Raspberry Pi theme, as shown in the following screenshot: Newell Raspberry Pi teapot Getting ready We shall use 3D models of a teapot (both teapot.obj and teapot.mdl) located in pi3d\models. Modeling a teapot is the traditional 3D equivalent of displaying Hello World. Computer graphics researcher Martin Newell first created the Newell Teapot in 1975 as a basic test model for his work. The Newell Teapot soon became the standard model to quickly check if a 3D rendering system is working correctly (it even appeared in Toy Story and a 3D episode of The Simpsons). Other models are available in the pi3d\models directory (monkey.obj/ mdl, which has been used later on, is available in the book's resource files). How to do it… Create and run the following 3dModel.py script: #!/usr/bin/python3 """ Wavefront obj model loading. Material properties set in 148 Chapter 5 mtl file. Uses the import pi3d method to load *everything* """ import demo import pi3d from math import sin, cos, radians KEY = {'ESC':27,'NONE':-1} # Setup display and initialise pi3d DISPLAY = pi3d.Display.create() #capture mouse and key presses mykeys = pi3d.Keyboard() mymouse = pi3d.Mouse(restrict = False) mymouse.start() def main(): #Model textures and shaders shader = pi3d.Shader("uv_reflect") bumptex = pi3d.Texture("textures/floor_nm.jpg") shinetex = pi3d.Texture("textures/stars.jpg") # load model #mymodel = pi3d.Model(file_string='models/teapot.obj', z=10) mymodel = pi3d.Model(file_string='models/monkey.obj', z=10) mymodel.set_shader(shader) mymodel.set_normal_shine(bumptex, 4.0, shinetex, 0.5) #Create environment box flatsh = pi3d.Shader("uv_flat") ectex = pi3d.loadECfiles("textures/ecubes","sbox") myecube = pi3d.EnvironmentCube(size=900.0, maptype="FACES", name="cube") myecube.set_draw_details(flatsh, ectex) CAMERA = pi3d.Camera.instance() rot = 0.0 # rotation of camera tilt = 0.0 # tilt of camera k = KEY['NONE'] omx, omy = mymouse.position() while DISPLAY.loop_running() and not k == KEY['ESC']: k = mykeys.read() #Rotate camera - camera steered by mouse mx, my = mymouse.position() rot -= (mx-omx)*0.8 149 Creating 3D Graphics tilt += (my-omy)*0.8 omx = mx omy = my CAMERA.reset() CAMERA.rotate(tilt, rot, 0) #Rotate object mymodel.rotateIncY(2.0) mymodel.rotateIncZ(0.1) mymodel.rotateIncX(0.3) #Draw objects mymodel.draw() myecube.draw() try: main() finally: mykeys.close() mymouse.stop() DISPLAY.destroy() print("Closed Everything. END") #End How it works... Like the 3dObject.py example, we define the DISPLAY shader (this time using uv_ reflect) and some additional textures—bumptex (floor_nm.jpg) and shinetex (stars.jpg)—to use later. We define a model that we want to import, placing it at z=10 (if no coordinates are given, it will be placed at (0,0,0). Since we do not specify a camera position, the default will place it within the view (see the section regarding the camera for more details). We apply the shader using the set_shader() function. Next, we add some textures and effects using bumptex as a surface texture (scaled by 4). We apply an extra shiny effect using shinetex and apply a reflection strength of 0.5 (the strength ranges from 0.0, the weakest, to 1.0, the strongest) using the set_normal_shine() function. If you look closely at the surface of the model, the bumptex texture provides additional surface detail and the shinetex texture can be seen as the reflection on the surface. To display our model within something more interesting than a default blue space, we create an EnvironmentCube object. This defines a large space that has a special texture applied to the inside space (in this instance, it will load the sbox_front/back/bottom/left and sbox_right images from the textures\ecubes directory), so it effectively encloses the objects within. The result is that you get a pleasant backdrop for your object. 150 Chapter 5 Again, we define a default CAMERA object with rot and tilt variables to control the view. Within the DISPLAY.loop_running() section, we can control the view of the CAMERA object using the mouse and rotate the model on its axis at different rates to let it spin and show all its sides (using the RotateIncX/Y/Z() function to specify the rate of rotation). Finally, we ensure that the DISPLAY is updated by drawing the model and the environment cube. There's more… We can create a wide range of objects to place within our simulated environment. Pi3D provides methods to import our own models and apply multiple textures to them. Creating or loading your own objects If you wish to use your own models in this example, you shall need to create one in the correct format; Pi3D supports obj (wavefront object files) and egg (Panda3D). An excellent, free, 3D modeling program is called Blender (available at http://www. blender.org). There are lots of examples and tutorials on their website to get you started with basic modeling (http://www.blender.org/education-help/tutorials). Pi3D model support is limited and will not support all the features that Blender can embed in an exported model, for example, deformable meshes. Therefore, only basic multipart models are supported. There are a few steps required to simplify the model so it can be loaded by Pi3D. To convert an .obj model to use with Pi3D, proceed with the following steps: 1. Create or load a model in Blender—try starting with a simple object before attempting more complex models. 2. Select each Object and switch to Edit mode (press Tab). 3. Select all vertices (press A) and uv-map them (press U and then select Unwrap). 4. Return to Object mode (press Tab). 5. Export it as obj—from the File menu at the top, select Export and then Wavefront (.obj). Ensure that Include Normals is also checked in the list of options in the bottom-left list. 6. Click on Save and place the .obj and .mtl files in the pi3d\models directory, and ensure that you update the script with the model's filename, as follows: mymodel = pi3d.Model(file_string='models/monkey.obj', name='monkey', z=4) 151 Creating 3D Graphics When you run your updated script, you will see your model displayed in the 3D space. For example, the monkey.obj model is shown in the following screenshot: A monkey head model created in Blender and displayed by Pi3D Changing the object's textures and .mtl files The texture that is applied to the surface of the model is contained within the .mtl file of the model. This file defines the textures and how they are applied as set by the modeling software. Complex models may contain multiple textures for various parts of the object. If no material is defined, the first texture in the shader is used (in our example, this is the bumptex texture). To add a new texture to the object, add (or edit) the following line in the .mtl file (that is, to use water.jpg): map_Kd ../textures/water.jpg More information about .mtl files and .obj files can be found at the following Wikipedia link: https://en.wikipedia.org/wiki/Wavefront_.obj_file 152 Chapter 5 Taking screenshots The Pi3D library includes a useful screenshot function to capture the screen in a .jpg or .png file. We can add a new key event to trigger it and call pi3d.screenshot("filename. jpg") to save an image (or use a counter to take multiple screenshots), as shown in the following code: shotnum = 0 #Set counter to 0 while DISPLAY.loop_running() ... if inputs.key_state("KEY_P"): while inputs.key_state("KEY_P"): # wait for key to go up inputs.do_input_events() pi3d.screenshot("screenshot%04d.jpg"%( shotnum)) shotnum += 1 ... Creating a 3D world to roam in Now that we are able to create models and objects within our 3D space, as well as generate backgrounds, we may want to create a more interesting environment within which to place them. 3D terrain maps provide an elegant way to define very complex landscapes. The terrain is defined using a grayscale image to set the elevation of the land. The following example shows how we can define our own landscape and simulate flying over it, or even walk on its surface: A 3D landscape generated from a terrain map 153 Creating 3D Graphics Getting ready You will need to place the Map.png file (available in the book resource files) in the pi3d/ textures directory of the Pi3D library. Alternatively, you can use one of the elevation maps already present—replace the reference to Map.png with another one of the elevation maps, such as testislands.jpg. How to do it… Create the following 3dWorld.py script: #!/usr/bin/python3 from __future__ import absolute_import, division from __future__ import print_function, unicode_literals """ An example of generating a 3D environment using a elevation map """ from math import sin, cos, radians import demo import pi3d KEY = {'R':114,'S':115,'T':116,'W':119,'ESC':27,'NONE':-1} DISPLAY = pi3d.Display.create(x=50, y=50) #capture mouse and key presses mykeys = pi3d.Keyboard() mymouse = pi3d.Mouse(restrict = False) mymouse.start() def limit(value,min,max): if (value < min): value = min elif (value > max): value = max return value def main(): CAMERA = pi3d.Camera.instance() tex = pi3d.Texture("textures/grass.jpg") flatsh = pi3d.Shader("uv_flat") # Create elevation map mapwidth,mapdepth,mapheight = 200.0,200.0,50.0 mymap = pi3d.ElevationMap("textures/Map.png", width=mapwidth, depth=mapdepth, height=mapheight, divx=128, divy=128, ntiles=20) 154 Chapter 5 mymap.set_draw_details(flatsh, [tex], 1.0, 1.0) rot = 0.0 # rotation of camera tilt = 0.0 # tilt of camera height = 20 viewhight = 4 sky = 200 xm,ym,zm = 0.0,height,0.0 k = KEY['NONE'] omx, omy = mymouse.position() onGround = False # main display loop while DISPLAY.loop_running() and not k == KEY['ESC']: CAMERA.reset() CAMERA.rotate(-tilt, rot, 0) CAMERA.position((xm,ym,zm)) mymap.draw() mx, my = mymouse.position() rot -= (mx-omx)*0.8 tilt += (my-omy)*0.8 omx = mx omy = my #Read keyboard keys k = mykeys.read() if k == KEY['W']: xm -= sin(radians(rot)) zm += cos(radians(rot)) elif k == KEY['S']: xm += sin(radians(rot)) zm -= cos(radians(rot)) elif k == KEY['R']: ym += 2 onGround = False elif k == KEY['T']: ym -= 2 ym -= 0.1 #Float down! #Limit the movement xm = limit(xm,-(mapwidth/2),mapwidth/2) zm = limit(zm,-(mapdepth/2),mapdepth/2) if ym >= sky: ym = sky #Check onGround 155 Creating 3D Graphics ground = mymap.calcHeight(xm, zm) + viewhight if (onGround == True) or (ym <= ground): ym = mymap.calcHeight(xm, zm) + viewhight onGround = True try: main() finally: mykeys.close() mymouse.stop() DISPLAY.destroy() print("Closed Everything. END") #End How it works… Once we have defined the display, camera, textures, and shaders that we are going to use, we can define the ElevationMap object. It works by assigning a height to the terrain image based on the pixel value of selected points of the image. For example, a single line of an image will provide a slice of the ElevationMap object and a row of elevation points on the 3D surface: Mapping the map.png pixel shade to the terrain height 156 Chapter 5 We create an ElevationMap object by providing the filename of the image we will use for the gradient information (textures/Map.png), and we also create the dimensions of the map (width, depth, and height—which is how high the white spaces will be compared to the black spaces): The light parts of the map will create high points and the dark ones will create low points The Map.png texture provides an example terrain map, which is converted into a three-dimensional surface. We also specify divx and divy, which determine how much detail of the terrain map is used (how many points from the terrain map are used to create the elevation surface). Finally, ntiles specifies that the texture used will be scaled to fit 20 times across the surface. Within the main DISPLAY.loop_running() section, we will control the camera, draw ElevationMap, respond to inputs, and limit movements in our space. 157 Creating 3D Graphics As before, we use a Keyboard object to capture mouse movements and translate them to control the camera. We will also use mykeys.read() to determine if W, S, R, and T have been pressed, which allow us to move forward and backwards, as well as rise up and down. To allow easy conversion between the values returned from the Keyboard object and their equivalent meaning, we will use a Python dictionary: KEY = {'R':114,'S':115,'T':116,'W':119,'ESC':27,'NO NE':-1} The dictionary provides an easy way to translate between a given value and the resulting string. To access a key's value, we use KEY['W']. We also used a dictionary in Chapter 3, Displaying Photo Information in an Application, to translate between the image Exif TAG names and IDs. To ensure that we do not fall through the surface of the ElevationMap object when we move over it, we can use mymap.calcHeight() to provide us with the height of the terrain at a specific location (x,y,z). We can either follow the ground by ensuring the camera is set to equal this, or fly through the air by just ensuring that we never go below it. When we detect that we are on the ground, we ensure that we remain on the ground until we press R to rise again. Building 3D maps and mazes We've seen that the Pi3D library can be used to create lots of interesting objects and environments. Using some of the more complex classes (or by constructing our own), whole custom spaces can be designed for the user to explore. In the following example, we use a special module called Building, which has been designed to allow you to construct a whole building using a single image file to provide the layout: 158 Chapter 5 Explore the maze and find the sphere that marks the exit Getting ready You will need to ensure that you have the following files in the pi3d/textures directory: ff squareblocksred.png ff floor.png ff inside_map0.png, inside_map1.png, inside_map2.png These files are available as part of the book's resources placed in Chapter05\resource\ source_files\textures. 159 Creating 3D Graphics How to do it… Let's run the following 3dMaze.py script by performing the following steps: 1. First, we set up the keyboard, mouse, display, and settings for the model using the following code: #!/usr/bin/python3 """Small maze game, try to find the exit """ from math import sin, cos, radians import demo import pi3d from pi3d.shape.Building import Building, SolidObject from pi3d.shape.Building import Size, Position KEY = {'A':97,'D':100,'H':104,'R':114,'S':115,'T':116, 'W':119,'ESC':27,'APOST':39,'SLASH':47,'NONE':-1} # Setup display and initialise pi3d DISPLAY = pi3d.Display.create() #capture mouse and key presses mykeys = pi3d.Keyboard() mymouse = pi3d.Mouse(restrict = False) #Load shader shader = pi3d.Shader("uv_reflect") flatsh = pi3d.Shader("uv_flat") # Load textures ceilingimg = pi3d.Texture("textures/squareblocks4.png") wallimg = pi3d.Texture("textures/squareblocksred.png") floorimg = pi3d.Texture("textures/dunes3_512.jpg") bumpimg = pi3d.Texture("textures/mudnormal.jpg") startimg = pi3d.Texture("textures/rock1.jpg") endimg = pi3d.Texture("textures/water.jpg") # Create elevation map mapwidth = 1000.0 mapdepth = 1000.0 #We shall assume we are using a flat floor in this example mapheight = 0.0 mymap = pi3d.ElevationMap(mapfile="textures/floor.png", width=mapwidth, depth=mapdepth, height=mapheight, divx=64, divy=64) mymap.set_draw_details(shader,[floorimg, bumpimg],128.0, 0.0) 160 Chapter 5 levelList = ["textures/inside_map0.png","textures/inside_map1. png", "textures/inside_map2.png"] avhgt = 5.0 aveyelevel = 4.0 MAP_BLOCK = 15.0 aveyeleveladjust = aveyelevel - avhgt/2 PLAYERHEIGHT = (mymap.calcHeight(5, 5) + avhgt/2) #Start the player in the top-left corner startpos = [(8*MAP_BLOCK),PLAYERHEIGHT,(8*MAP_BLOCK)] endpos = [0,PLAYERHEIGHT,0] #Set the end pos in the centre person = SolidObject("person", Size(1, avhgt, 1), Position(startpos[0],startpos[1],startpos[2]), 1) #Add spheres for start and end, end must also have a solid object #so we can detect when we hit it startobject = pi3d.Sphere(name="start",x=startpos[0], y=startpos[1]+avhgt,z=startpos[2]) startobject.set_draw_details(shader, [startimg, bumpimg], 32.0, 0.3) endobject = pi3d.Sphere(name="end",x=endpos[0], y=endpos[1],z=endpos[2]) endobject.set_draw_details(shader, [endimg, bumpimg], 32.0, 0.3) endSolid = SolidObject("end", Size(1, avhgt, 1), Position(endpos[0],endpos[1],endpos[2]), 1) mazeScheme = {"#models": 3, (1,None): [["C",2]], (0,1,"edge"): [["W",1]], (1,0,"edge"): [["W",1]], (0,1):[["W",0]]} #white cell : Ceiling #white cell on edge next # black cell : Wall #black cell on edge next # to white cell : Wall #white cell next # to black cell : Wall details = [[shader, [wallimg], 1.0, 0.0, 4.0, 16.0], [shader, [wallimg], 1.0, 0.0, 4.0, 8.0], [shader, [ceilingimg], 1.0, 0.0, 4.0, 4.0]] arialFont = pi3d.Font("fonts/FreeMonoBoldOblique.ttf", "#ffffff", font_size=10) 161 Creating 3D Graphics 2. We then create functions to allow us to reload the levels and display messages to the player using the following code: def loadLevel(next_level): print(">>> Please wait while maze is constructed...") next_level=next_level%len(levelList) building = pi3d.Building(levelList[next_level], 0, 0, mymap, width=MAP_BLOCK, depth=MAP_BLOCK, height=30.0, name="", draw_details=details, yoff=-15, scheme=mazeScheme) return building def showMessage(text,rot=0): message = pi3d.String(font=arialFont, string=text, x=endpos[0],y=endpos[1]+(avhgt/4), z=endpos[2], sx=0.05, sy=0.05,ry=-rot) message.set_shader(flatsh) message.draw() 3. Within the main function, we set up the 3D environment and draw all the objects using the following code: def main(): #Load a level level=0 building = loadLevel(level) lights = pi3d.Light(lightpos=(10, -10, 20), lightcol =(0.7, 0.7, 0.7), lightamb=(0.7, 0.7, 0.7)) rot=0.0 tilt=0.0 #capture mouse movements mymouse.start() omx, omy = mymouse.position() CAMERA = pi3d.Camera.instance() while DISPLAY.loop_running() and not \ inputs.key_state("KEY_ESC"): CAMERA.reset() CAMERA.rotate(tilt, rot, 0) CAMERA.position((person.x(), person.y(), person.z() - aveyeleveladjust)) #draw objects 162 Chapter 5 person.drawall() building.drawAll() mymap.draw() startobject.draw() endobject.draw() #Apply the light to all the objects in the building for b in building.model: b.set_light(lights, 0) mymap.set_light(lights, 0) #Get mouse position mx, my = mymouse.position() rot -= (mx-omx)*0.8 tilt += (my-omy)*0.8 omx = mx omy = my xm = person.x() ym = person.y() zm = person.z() 4. Finally, we monitor for key presses, handle any collisions with objects, and move within the maze as follows: #Read keyboard keys k = mykeys.read() if k == KEY['APOST']: #' Key tilt -= 2.0 elif k == KEY['SLASH']: #/ Key tilt += 2.0 elif k == KEY['A']: rot += 2.0 elif k == KEY['D']: rot -= 2.0 elif k == KEY['H']: #Use point_at as help - will turn the player to face # the direction of the end point tilt, rot = CAMERA.point_at([endobject.x(), endobject.y(), endobject.z()]) elif k == KEY['W']: xm -= sin(radians(rot)) zm += cos(radians(rot)) elif k == KEY['S']: 163 Creating 3D Graphics xm += sin(radians(rot)) zm -= cos(radians(rot)) NewPos = Position(xm, ym, zm) collisions = person.CollisionList(NewPos) if collisions: #If we reach the end, reset to start position! for obj in collisions: if obj.name == "end": #Required to remove the building walls from the # solidobject list building.remove_walls() showMessage("Loading Level",rot) DISPLAY.loop_running() level+=1 building = loadLevel(level) showMessage("") person.move(Position(startpos[0],startpos[1], startpos[2])) else: person.move(NewPos) try: main() finally: mykeys.close() mymouse.stop() DISPLAY.destroy() print("Closed Everything. END") #End How it works... We define many of the elements we used in the preceding examples, such as the display, textures, shaders, font, and lighting. We also define the objects, such as the building itself, the ElevationMap object, as well as the start and end points of the maze. We also use SolidObjects to help detect movement within the space. See the Using SolidObjects to detect collisions subsection in the There's more… section of this recipe for more information. 164 Chapter 5 Finally, we create the actual Building object based on the selected map image (using the loadLevel() function) and locate the camera (which represents our first-person viewpoint) at the start. See the The Building module subsection in the There's more… section of this recipe for more information. Within the main loop, we draw all the objects in our space and apply the lighting effects. We will also monitor for movement in the mouse (to control the tilt and rotation of the camera) or the keyboard to move the player (or exit/provide help). The controls are as follows: ff Mouse movement: This changes the camera tilt and rotation. ff ' or / key: This changes the camera to tilt either downwards or upwards. ff A or D: This changes the camera to rotate from left to right or vice versa. ff W or S: This moves the player forwards or backwards. ff H: This helps the player by rotating them to face the end of the maze. The useful CAMERA.point_at() function is used to quickly rotate and tilt the camera's viewpoint towards the provided coordinates (the end position). Whenever the player moves, we check if the new position (NewPos) collides with another SolidObject using CollisionList(NewPos). The function will return a list of any other SolidObjects that overlap the coordinates provided. If there are no SolidObjects in the way, we make the player move; otherwise, we check to see if one of the SolidObject's names is the end object, in which case we have reached the end of the maze. When the player reaches the end, we remove the walls from the old Building object and display a loading message. If we don't remove the walls, all the SolidObjects belonging to the previous Building will still remain, creating invisible obstacles in the next level. 165 Creating 3D Graphics We use the showMessage() function to inform the user that the next level will be loaded soon (since it can take a while for the building object to be constructed). We need to ensure that we call DISPLAY.loop_running() after we draw the message. This ensures it is displayed on screen before we start loading the level (after which the person will be unable to move while loading takes place). We need to ensure that the message is always facing the player regardless of which of their sides collides with the end object by using the camera rotation (rot) for its angle. When the exit ball is found, the next level is loaded When the next level in the list has been loaded (or the first level has been loaded again when all the levels have been completed), we replace the message with a blank one to remove it and reset the person's position back to the start. You can design and add your own levels by creating additional map files (20 x 20 PNG files with walls marked out with black pixels and walkways in white) and listing them in levelList. The player will start at the top-left corner of the map, and the exit is placed at the center. You will notice that loading the levels can take quite a long time; this is the relatively slow ARM processor in the Raspberry Pi performing all the calculations required to construct the maze and locate all the components. As soon as the maze has been built, the more powerful GPU takes over, which results in fast and smooth graphics as the player explores the space. 166 Chapter 5 This recipe demonstrates the difference between the original Raspberry Pi processor and the Raspberry Pi 2. The Raspberry Pi 2 takes around 1 minute 20 seconds to load the first level, while the original Raspberry Pi can take up to 4 minutes 20 seconds. The Raspberry Pi 3 takes a stunning 4 seconds to load the same level. There's more... The preceding example creates a building for the player to explore and interact with. In order to achieve this, we use the Building module of Pi3D to create a building and use SolidObject to detect collisions. The Building module The pi3d.Building module allows you to define a whole level or floor of a building using map files. Like the terrain maps used in the preceding example, the color of the pixels will be converted into different parts of the level. In our case, black is for the walls and white is for the passages and halls, complete with ceilings: The building layout is defined by the pixels in the image 167 Creating 3D Graphics The sections built by the Building object are defined by the Scheme used. The Scheme is defined by two sections, by the number of models, and then by the definitions for various aspects of the model, as seen in the following code: mazeScheme = {"#models": 3, (1,None): [["C",2]], #white (0,1,"edge"): [["W",1]], #white (1,0,"edge"): [["W",1]], #black (0,1):[["W",0]]} #white cell cell cell cell : Ceiling on edge by black cell : Wall on edge by white cell : Wall next to black cell : Wall The first tuple defines the type of cell/square that the selected model should be applied to. Since there are two pixel colors in the map, the squares will either be black (0) or white (1). By determining the position and type of a particular cell/square, we can define which models (wall, ceiling, or roof) we want to apply. We define three main types of cell/square location: ff A whole square (1,None): This is a white cell representing open space in the building. ff One cell bordering another, on the edge (0,1,"edge"): This is a black cell next to a white one on the map edge. This also includes (1,0,"edge"). This will represent the outer wall of the building. ff Any black cell that is next to a white cell (0,1): This will represent all the internal walls of the building. Next, we allocate a type of object(s) to be applied for that type (W or C): ff Wall (W): This is a vertical wall that is placed between the specified cells (such as between black and white cells). ff Ceiling (C): This is a horizontal section of the ceiling to cover the current cell. ff Roof (R): This is an additional horizontal section that is placed slightly above the ceiling to provide a roofing effect. It is typically used for buildings that may need to be viewed from the outside (this is not used in our example). ff Ceiling Edge (CE): This is used to join the ceiling sections to the roof around the edges of the building (it is not used in our example since ours is an indoor model). Finally, we specify the model that will be used for each object. We are using three models in this example (normal walls, walls on an edge, and the ceiling), so we can define the model used by specifying 0, 1, or 2. 168 Chapter 5 Each of the models are defined in the details array, which allows us to set the required textures and shaders for each one (this contains the same information that would normally be set by the .set_draw_details() function), as shown in the following code: details = [[shader, [wallimg], 1.0, 0.0, 4.0, 16.0], [shader, [wallimg], 1.0, 0.0, 4.0, 8.0], [shader, [ceilingimg], 1.0, 0.0, 4.0, 4.0]] In our example, the inside walls are allocated to the wallimg texture (textures/ squareblocksred.png) and the ceilings are allocated to the ceilingimg texture (textures/squareblocks4.png). You may be able to note from the following screenshot that we can apply different texture models (in our case, a slightly different scaling) to the different types of blocks. The walls that border the outside of the maze (with the edge identifier) will use the wallimg model texture scaled by 4x8 (details[1]) while the same model texture will be scaled 4x16 for the internal walls (details[0]): The outward facing wall on the left has a different scaling applied compared to the other walls Both scheme and draw_details are set when the pi3d.Building object is created, as shown in the following code: building = pi3d.Building(levelList[next_level], 0, 0, mymap, width=MAP_BLOCK, depth=MAP_BLOCK, height=30.0, name="", draw_details=details, yoff=-15, scheme=mazeScheme) 169 Creating 3D Graphics Using the map file (levelList[next_level]), the scheme (mazeScheme), and draw details (details), the entire building is created within the environment: An overhead view of the 3D maze we created Although we use just black and white in this example, other colored pixels can also be used to define additional block types (and therefore different textures, if required). If another color (such as gray) is added, the indexing of the color mapping is shifted so that black blocks are referenced as 0, the new colored blocks as 1, and the white blocks as 2. See the Silo example in the Pi3D demos for details. We also need to define an ElevationMap object—mymap. The pi3d.Building module makes use of the ElevationMap object's calcHeight() function to correctly place the walls on top of the ElevationMap object's surface. In this example, we will apply a basic ElevationMap object using textures/floor.png, which will generate a flat surface that the Building object will be placed on. 170 Chapter 5 Using SolidObjects to detect collisions In addition to the Building object, we will define an object for the player and also define two objects to mark the start and end points of the maze. Although the player's view is the firstperson viewpoint (that is, we don't actually see them since the view is effectively through their eyes), we need to define a SolidObject to represent them. A SolidObject is a special type of invisible object that can be checked to determine if the space that would be occupied by one SolidObject has overlapped another. This will allow us to use person.CollisionList(NewPos) to get a list of any other SolidObjects that the person object will be in contact with at the NewPos position. Since the Building class defines SolidObjects for all the parts of the Building object, we will be able to detect when the player tries to move through a wall (or, for some reason, the roof/ceiling) and stop them from moving through it. We also use SolidObjects for the start and end locations in the maze. The place where the player starts is set as the top-left corner of the map (the white-space pixel from the top left of the map) and is marked by the startpos object (a small pi3d.Sphere with the rock1.jpg texture) placed above the person's head. The end of the maze is marked with another pi3d. Sphere object (with the water.jpg texture) located at the center of the map. We also define another SolidObject at the end so that we can detect when the player reaches it and collides with it (and load the next level!). 171 6 Using Python to Drive Hardware In this chapter, we will cover the following topics: ff Controlling an LED ff Responding to a button ff A controlled shutdown button ff The GPIO keypad input ff Multiplexed color LEDs ff Writing messages using Persistence of Vision Introduction One of the key features of a Raspberry Pi computer that sets it apart from most other home/office computers is that it has the ability to directly interface with other hardware. The hardware General Purpose Input/Output (GPIO) pins on the Raspberry Pi can control a wide range of low-level electronics, from Light Emitting Diodes (LEDs) to switches, sensors, motors, servos, and even extra displays. This chapter will focus on connecting the Raspberry Pi with some simple circuits and getting to grips with using Python to control and respond to the connected components. The Raspberry Pi hardware interface consists of 40 pins located along one side of the board. 173 Using Python to Drive Hardware The GPIO pins and their layout will vary slightly according to the particular model you have. The Raspberry Pi 2 and the Raspberry Pi 1 Model A Plus and B Plus all have the same 40-pin layout. The older Raspberry Pi 1 models (non-plus types) have a 26-pin header, which is the same as the 1-26 pins of the newer models. Raspberry Pi 2 and Raspberry Pi Model Plus GPIO header pins (pin functions) The layout of the connector is shown in the previous diagram; the pin numbers are shown as seen from pin 1 of the GPIO header. 174 Chapter 6 Pin 1 is at the end that is nearest to the SD card, as shown in the following image: The Raspberry Pi GPIO header location Care should be taken when using the GPIO header, since it also includes power pins (3V3 and 5V) as well as ground pins (GND). All of the GPIO pins can be used as standard GPIO, but several also have special functions; these are labeled and highlighted with different colors. It is common for engineers to use a 3V3 notation to specify values in schematics to avoid using decimal places that could easily be missed (using 33V rather than 3.3V would cause severe damage). The same can applied to other values such as resistors, so for example, 1.2k ohms can be written as 1k2 ohms. There are TX and RX pins that are used for serial RS232 communications, and with the aid of a voltage level convertor, information can be transferred via a serial cable to another computer or device. We have SDA and SCL pins that are able to support a two-wire bus communication protocol called I2C (on Model Plus and Raspberry Pi 2 boards there are two I2C channels: channel 1 ARM is for general use while channel 0 VC is typically used for identifying Hardware Attached on Top (HAT) modules). There are also the SPI MOSI, SPI MISO, SPI SCLK, SPI CE0, and SPI CE1 pins, which support another type of bus protocol called SPI for high-speed data. Finally, we have PWM0/1, which allows a pulse width modulation signal to be generated, which is useful for servos and generating analog signals. 175 Using Python to Drive Hardware However, we will focus on using just the standard GPIO functions in this chapter. The GPIO pin layout is shown in the following diagram: Raspberry Pi GPIO header pins (GPIO.BOARD and GPIO.BCM) 176 Chapter 6 The Raspberry Pi Rev 2 (pre-July 2014) has the following differences to the Raspberry Pi 2 GPIO layout: ff 26 GPIO pins header (matching the first 26 pins) ff An additional secondary set of eight holes (P5) located next to the pin header. The details are as follows: Raspberry Pi Rev 2 P5 GPIO header pins The original Raspberry Pi Rev 1 (pre-Oct 2012) has only 26 GPIO pins in total (matching the first 26 pins of the current Raspberry Pi, except for the following details): Raspberry Pi Rev 1 GPIO header differences The RPi.GPIO library can reference the pins on the Raspberry Pi using one of two systems. The numbers shown in the center are the physical position of the pins and are also the numbers referenced by RPi.GPIO when in the GPIO.BOARD mode. The numbers on the outside (GPIO.BCM) are the actual references for the physical ports of the processor to which the pins are wired (which is why they are not in any specific order). They are used when the mode is set to GPIO.BCM and allow control of the GPIO header pins and also any peripherals connected to other GPIO lines. This includes the LED on the add-on camera on BCM GPIO 4 and the status LED on the board. However, this can also include the GPIO lines used for reading/writing to the SD card, which would cause serious errors if interfered with. If you use other programming languages to access the GPIO pins, the numbering scheme may be different, so it will be helpful if you are aware of the BCM GPIO references, which refer to the physical GPIO port of the processor. Be sure to check out the Appendix, Hardware and Software List, which lists all the items used in this chapter and the places you can obtain them from. 177 Using Python to Drive Hardware Controlling an LED The hardware equivalent of hello world is an LED flash, which is a great test to ensure that everything is working and that you have wired it correctly. To make it a little more interesting, I've suggested using an RGB LED (it has red, green, and blue LEDs combined into a single unit), but feel free to use separate LEDs if that is all you have available. Getting ready You will need the following equipment: ff 4 x DuPont female to male patch wires ff Mini breadboard (170 tie points) or a larger one ff RGB LED (common cathode)/3 standard LEDs (ideally red/green/blue) ff Breadboarding wire (solid core) ff 3 x 470 ohm resistors Each of the previous components should only cost a few dollars and can be reused for other projects afterwards. The breadboard is a particularly useful item that allows you to try out your own circuits without needing to solder them. The diagrams of an RGB LED, standard LED, and RGB circuit 178 Chapter 6 The following diagram shows the breadboard circuitry: The wiring of an RGB LED/standard LEDs connected to the GPIO header There are several variations of RGB LEDs available, so check the datasheet of your component to confirm the pin order and type you have. Some are Red, Blue, and Green (RBG), so ensure that you wire accordingly or adjust the RGB_ pin settings in the code. You can also get common anode variants, which will require the anode to be connected to 3V3 (GPIO-Pin1) for it to light up (and require RGB_ENABLE and RGB_DISABLE to be set to 0 and 1). The breadboard and component diagrams of this book have been created using a free tool called Fritzing (www.fritzing.org); it is great for planning your own Raspberry Pi projects. 179 Using Python to Drive Hardware How to do it… Create the ledtest.py script as follows: #!/usr/bin/python3 #ledtest.py import time import RPi.GPIO as GPIO # RGB LED module #HARDWARE SETUP # GPIO # 2[======XRG=B==]26[=======]40 # 1[=============]25[=======]39 # X=GND R=Red G=Green B=Blue #Setup Active States #Common Cathode RGB-LED (Cathode=Active Low) RGB_ENABLE = 1; RGB_DISABLE = 0 #LED CONFIG - Set GPIO Ports RGB_RED = 16; RGB_GREEN = 18; RGB_BLUE = 22 RGB = [RGB_RED,RGB_GREEN,RGB_BLUE] def led_setup(): #Setup the wiring GPIO.setmode(GPIO.BOARD) #Setup Ports for val in RGB: GPIO.setup(val,GPIO.OUT) def main(): led_setup() for val in RGB: GPIO.output(val,RGB_ENABLE) print("LED ON") time.sleep(5) GPIO.output(val,RGB_DISABLE) print("LED OFF") try: main() finally: GPIO.cleanup() print("Closed Everything. END") #End 180 Chapter 6 The RPi.GPIO library will require sudo permissions to access the GPIO pin hardware, so you will need to run the script using the following command: sudo python3 ledtest.py When you run the script, you should see the red, green, and blue parts of the LED (or each LED, if using separate ones) light up in turn. If not, double-check your wiring or confirm the LED is working by temporarily connecting the red, green, or blue wire to the 3V3 pin (pin 1 of the GPIO header). The sudo command is required for most hardware-related scripts because it isn't normal for users to directly control hardware at such a low level. For example, setting or clearing a control pin that is part of the SD card controller could corrupt data being written to it. Therefore, for security purposes, super user permissions are required to stop programs from using hardware by accident (or with malicious intent). How it works… To access the GPIO pins using Python, we import RPi.GPIO, which allows direct control of the pins through the module functions. We also require the time module to pause the program for a set number of seconds. We define values for the LED wiring and active states (see Controlling the GPIO current in the There's more… section of this recipe). Before the GPIO pins are used by the program, we need to set them up by specifying the numbering method (GPIO.BOARD) and the direction—GPIO.OUT or GPIO.IN (in this case, we set all the RGB pins to outputs). If a pin is configured as an output, we will be able to set the pin state; similarly, if it is configured as an input, we will be able to read the pin state. Next, we control the pins using GPIO.ouput() by stating the number of the GPIO pin and the state we want it to be in (1 = high/on and 0 = low/off). We switch each LED on, wait 5 seconds, and then switch it back off. Finally, we use GPIO.cleanup() to return the GPIO pins back to their original default state and release control of the pins for use by other programs. There's more… Using the GPIO pins on the Raspberry Pi must be done with care since these pins are directly connected to the main processor of the Raspberry Pi without any additional protection. Caution must be used, as any incorrect wiring will probably damage the Raspberry Pi processor and cause it to stop functioning altogether. 181 Using Python to Drive Hardware Alternatively, you could use one of the many modules available that plug directly into the GPIO header pins (reducing the chance of wiring mistakes). For example, the Pi-Stop is a simple pre-built LED board that simulates a set of traffic lights, designed to be a stepping stone for those interested in controlling hardware but want to avoid the risk of damaging their Raspberry Pi. After the basics have been mastered, it also makes an excellent indicator to aid debugging. Just ensure you update the LED CONFIG pin references in the ledtest.py script to reference the pin layout and location used for the hardware you are using. See the Appendix, Hardware and Software List, for a list of Raspberry Pi hardware retailers. 182 Chapter 6 Controlling the GPIO current Each GPIO pin is only able to handle a certain current before it will burn out (not greater than 16 mA from a single pin or 30 mA in total), and similarly, the RGB LED should be limited to no more than 100 mA. By adding a resistor before or after an LED, we will be able to limit the current that will be passed through it and also control how bright it is (more current will equal a brighter LED). Since we may wish to drive more than one LED at a time, we typically aim to set the current as low as we can get away with while still providing enough power to light up the LED. We can use Ohm's law to tell us how much resistance to use to provide a particular current. The law is as shown in the following diagram: I (current through the components, amperes) V I R (resistance of the component, ohms) R V (voltage across the component, volts) The triangle shows: V = I x R I = V R = V R I Ohm's law describes the relationship between the current, resistance, and voltage in electrical circuits We will aim for a minimum current (3 mA) and maximum current (16 mA), while still producing a reasonably bright light from each of the LEDs. To get a balanced output for the RGB LEDs, I tested different resistors until they provided a near white light (when viewed through a card). A 470 ohm resistor was selected for each one (your LEDs may differ slightly). Resistors are needed to limit the current that passes through the LEDs 183 Using Python to Drive Hardware The voltage across the resistor is equal to the GPIO voltage (Vgpio = 3.3V) minus the voltage drop on the particular LED (Vfwd); we can then use this resistance to calculate the current used by each of the LEDs, as shown in the following diagram: We can calculate the current drawn by each of the LEDs Responding to a button Many applications using the Raspberry Pi require that actions are activated without a keyboard and screen attached to it. The GPIO pins provide an excellent way for the Raspberry Pi to be controlled by your own buttons and switches without a mouse/keyboard and screen. Getting ready You will need the following equipment: ff 2 x DuPont female to male patch wires ff Mini breadboard (170 tie points) or a larger one ff Push button switch (momentary close) or a wire connection to make/break the circuit ff Breadboarding wire (solid core) ff 1k ohm resistor The switches are as seen in the following diagram: The push button switch and other types of switch 184 Chapter 6 The switches used in the following examples are single pole single throw (SPST) momentary close push button switches. Single pole (SP) means that there is one set of contacts that makes a connection. In the case of the push switch used here, the legs on each side are connected together with a single pole switch in the middle. A double pole (DP) switch acts just like a single pole switch, except that the two sides are separated electrically, allowing you to switch two separate components on/off at the same time. Single throw (ST) means the switch will make a connection with just one position; the other side will be left open. Double throw (DT) means both positions of the switch will connect to different parts. Momentary close means that the button will close the switch when pressed and automatically open it when released. A latched push button switch will remain closed until it is pressed again. The layout of the button circuit We will use sound in this example, so you will also need speakers or headphones attached to the audio socket of the Raspberry Pi. You will need to install a program called flite using the following command, which will let us make the Raspberry Pi talk: sudo apt-get install flite After it has been installed, you can test it with the following command: sudo flite -t "hello I can talk" If it is a little too quiet (or too loud), you can adjust the volume (0-100 percent) using the following command: amixer set PCM 100% 185 Using Python to Drive Hardware How to do it… Create the btntest.py script as follows: #!/usr/bin/python3 #btntest.py import time import os import RPi.GPIO as GPIO #HARDWARE SETUP # GPIO # 2[==X==1=======]26[=======]40 # 1[=============]25[=======]39 #Button Config BTN = 12 def gpio_setup(): #Setup the wiring GPIO.setmode(GPIO.BOARD) #Setup Ports GPIO.setup(BTN,GPIO.IN,pull_up_down=GPIO.PUD_UP) def main(): gpio_setup() count=0 btn_closed = True while True: btn_val = GPIO.input(BTN) if btn_val and btn_closed: print("OPEN") btn_closed=False elif btn_val==False and btn_closed==False: count+=1 print("CLOSE %s" % count) os.system("flite -t '%s'" % count) btn_closed=True time.sleep(0.1) try: 186 Chapter 6 main() finally: GPIO.cleanup() print("Closed Everything. END") #End How it works… As in the previous recipe, we set up the GPIO pin as required, but this time as an input, and we also enable the internal pull-up resistor (see Pull-up and pull-down resistor circuits in the There's more… section of this recipe for more information) using the following code: GPIO.setup(BTN,GPIO.IN,pull_up_down=GPIO.PUD_UP) After the GPIO pin is set up, we create a loop that will continuously check the state of BTN using GPIO.input(). If the value returned is false, the pin has been connected to 0V (ground) through the switch, and we will use flite to count out loud for us each time the button is pressed. Since we have called the main function from within a try/finally condition, it will still call GPIO.cleanup() even if we close the program using Ctrl + Z. We use a short delay in the loop; this ensures that any noise from the contacts on the switch is ignored. This is because when we press the button, there isn't always perfect contact as we press or release it, and it may produce several triggers if we press it again too quickly. This is known as software debouncing; we ignore the bounce in the signal here. There's more… The Raspberry Pi GPIO pins must be used with care; voltages used for inputs should be within specific ranges, and any current drawn from them should be minimized using protective resistors. Safe voltages We must ensure that we only connect inputs that are between 0 (Ground) and 3.3V. Some processors use voltages between 0 and 5V, so extra components are required to interface safely with them. Never connect an input or component that uses 5V unless you are certain it is safe, or you will damage the GPIO ports of the Raspberry Pi. 187 Using Python to Drive Hardware Pull-up and pull-down resistor circuits The previous code sets the GPIO pins to use an internal pull-up resistor. Without a pull-up resistor (or pull-down resistor) on the GPIO pin, the voltage is free to float somewhere between 3.3V and 0V, and the actual logical state remains undetermined (sometimes 1 and sometimes 0). Raspberry Pi's internal pull-up resistors are 50k ohm-65k ohm and the pull-down resistors are 50k ohm-65k ohm. External pull-up/pull-down resistors are often used in GPIO circuits (as shown in the following diagram), typically using 10k ohm or larger for similar reasons (giving a very small current draw when not active). A pull-up resistor allows a small amount of current to flow through the GPIO pin and will provide a high voltage when the switch isn't pressed. When the switch is pressed, the small current is replaced by the larger one flowing to 0V, so we get a low voltage on the GPIO pin instead. The switch is active low and logic 0 when pressed. It works as shown in the following diagram: A pull-up resistor circuit 188 Chapter 6 Pull-down resistors work in the same way, except the switch is active high (the GPIO pin is logic 1 when pressed). It works as shown in the following diagram: A pull-down resistor circuit Protection resistors In addition to the switch, the circuit includes a resistor in series with the switch to protect the GPIO pin, as shown in the following diagram: A GPIO protective current-limiting resistor The purpose of the protection resistor is to protect the GPIO pin if it is accidentally set as an output rather than an input. Imagine, for instance, that we have our switch connected between the GPIO and ground. Now the GPIO pin is set as an output and switched on (driving it to 3.3V) as soon as we press the switch; without a resistor present, the GPIO pin will directly be connected to 0V. The GPIO will still try to drive it to 3.3V; this would cause the GPIO pin to burn out (since it would use too much current to drive the pin to the high state). If we use a 1k ohm resistor here, the pin is able to be driven high using an acceptable amount of current (I = V/R = 3.3/1k = 3.3mA). 189 Using Python to Drive Hardware A controlled shutdown button The Raspberry Pi should always be shut down correctly to avoid the SD card being corrupted (by losing power while performing a write operation to the card). This can pose a problem if you don't have a keyboard or screen connected (if you are running an automated program or controlling it remotely over a network and forget to turn it off) as you can't type the command or see what you are doing. By adding our own buttons and LED indicator, we can easily command a shutdown, reset, and startup again to indicate when the system is active. Getting ready You will need the following equipment: ff 3 x Dupont female to male patch wires ff Mini breadboard (170 tie points) or a larger one ff Push button switch (momentary close) ff General purpose LED ff 2 x 470-ohm resistors ff Breadboarding wire (solid core) The entire layout of the shutdown circuit will look as shown in the following figure: The controlled shutdown circuit layout 190 Chapter 6 How to do it… Create the shtdwn.py script as follows: #!/usr/bin/python3 #shtdwn.py import time import RPi.GPIO as GPIO import os # Shutdown Script DEBUG=True #Simulate Only SNDON=True #HARDWARE SETUP # GPIO # 2[==X==L=======]26[=======]40 # 1[===1=========]25[=======]39 #BTN CONFIG - Set GPIO Ports GPIO_MODE=GPIO.BOARD SHTDWN_BTN = 7 #1 LED = 12 #L def gpio_setup(): #Setup the wiring GPIO.setmode(GPIO_MODE) #Setup Ports GPIO.setup(SHTDWN_BTN,GPIO.IN,pull_up_down=GPIO.PUD_UP) GPIO.setup(LED,GPIO.OUT) def doShutdown(): if(DEBUG):print("Press detected") time.sleep(3) if GPIO.input(SHTDWN_BTN): if(DEBUG):print("Ignore the shutdown (<3sec)") else: if(DEBUG):print ("Would shutdown the RPi Now") GPIO.output(LED,0) time.sleep(0.5) GPIO.output(LED,1) if(SNDON):os.system("flite -t 'Warning commencing power down 3 2 1'") if(DEBUG==False):os.system("sudo shutdown -h now") if(DEBUG):GPIO.cleanup() 191 Using Python to Drive Hardware if(DEBUG):exit() def main(): gpio_setup() GPIO.output(LED,1) while True: if(DEBUG):print("Waiting for >3sec button press") if GPIO.input(SHTDWN_BTN)==False: doShutdown() time.sleep(1) try: main() finally: GPIO.cleanup() print("Closed Everything. END") #End To get this script to run automatically (once we have tested it), we can place the script in ~/bin (we can use cp instead of mv if we just want to copy it) and add it to crontab with the following code: mkdir ~/bin mv shtdwn.py ~/bin/shtdwn.py crontab –e At the end of the file, we add the following code: @reboot sudo python3 ~/bin/shtdwn.py How it works… This time, when we set up the GPIO pin, we define the pin connected to the shutdown button as an input and the pin connected to the LED as an output. We turn the LED on to indicate that the system is running. By setting the DEBUG flag to True, we can test the functionality of our script without causing an actual shutdown (by reading the terminal messages); we just need to ensure to set DEBUG to False when using the script for real. We enter a while loop and check every second to see whether the GPIO pin is set to LOW (the switch has been pressed); if so, we enter the doShutdown() function. 192 Chapter 6 The program will wait for 3 seconds and then test again to see whether the button is still being pressed. If the button is no longer being pressed, we return to the previous while loop. However, if it is still being pressed after 3 seconds, the program will flash the LED and trigger the shutdown (also providing an audio warning using flite). When we are happy with how the script is operating, we can disable the DEBUG flag (by setting it to False) and add the script to crontab. Crontab is a special program that runs in the background and allows us to schedule (at specific times, dates, or periodically) programs and actions when the system is started (@reboot). This allows the script to be started automatically every time the Raspberry Pi is powered up. When we press and hold the shutdown button for more than 3 seconds, it safely shuts down the system and enters a low power state (the LED switches off just before this, indicating it is safe to remove the power shortly after). To restart the Raspberry Pi, we briefly remove the power; this will restart the system, and the LED will light up when the Raspberry Pi has loaded. There's more… We can extend this example further using the reset header by adding extra functionality and making use of additional GPIO connections (if available). Resetting and rebooting Raspberry Pi The Raspberry Pi has holes for mounting a reset header (marked RUN on the Raspberry Pi 2 / 3 and P6 on the Raspberry Pi 1 Model B Rev 2 and Model As). The reset pin allows the device to be reset using a button rather than removing the micro USB connector each time to cycle the power: Raspberry Pi reset headers – on the left, Raspberry Pi Model A/B (Rev2), and on the right, Raspberry Pi 2 193 Using Python to Drive Hardware To make use of it, you will need to solder a wire or pin header to the Raspberry Pi and connect a button to it (or briefly touch a wire between the two holes each time). Alternatively, we can extend our previous circuit, as shown in the following diagram: The controlled shutdown circuit layout and reset button We can add this extra button to our circuit, which can be connected to the reset header (this is the hole nearest the middle on the Raspberry Pi 2 or closest to the edge on other models). This pin, when temporarily pulled low by connecting to ground (such as the hole next to it or by another ground point such as pin 6 of the GPIO header), will reset the Raspberry Pi and allow it to boot up again following a shutdown. Adding extra functions Since we now have the script monitoring the shutdown button all the time, we can add extra buttons/switches/jumpers to be monitored at the same time. This will allow us to trigger specific programs or set up particular states just by changing the inputs. The following example allows us to easily switch between automatic DHCP networking (the default networking setup) and using a direct IP address, as used in the Networking directly to a laptop or computer recipe of Chapter 1, Getting Started with a Raspberry Pi Computer, for direct LAN connections. Add the following components to the previous circuit: ff A 470-ohm resistor ff Two pin headers with a jumper connector (or optionally a switch) ff Breadboarding wire (solid core) 194 Chapter 6 After adding the previous components, our controlled shutdown circuit now looks as follows: The controlled shutdown circuit layout, reset button, and jumper pins In the previous script, we add an additional input to detect the status of the LAN_SWA pin (the jumper pins we added to the circuit) using the following code: LAN_SWA = 11 #2 Ensure that it is set up as an input (with a pull-up resistor) in the gpio_setup() function using the following code: GPIO.setup(LAN_SWA,GPIO.IN,pull_up_down=GPIO.PUD_UP) Add a new function to switch between the LAN modes, and read out the new IP address. The doChangeLAN() function checks if the status of the LAN_SWA pin has changed since the last call, and if so, it sets the network adaptor to DHCP or sets the direct LAN settings accordingly (and uses flite to speak the new IP setting if available). Finally, the LAN being set for direct connection causes the LED to flash slowly while that mode is active. Use the following code to do so: def doChangeLAN(direct): if(DEBUG):print("Direct LAN: %s" % direct) if GPIO.input(LAN_SWA) and direct==True: if(DEBUG):print("LAN Switch OFF") cmd="sudo dhclient eth0" 195 Using Python to Drive Hardware direct=False GPIO.output(LED,1) elif GPIO.input(LAN_SWA)==False and direct==False: if(DEBUG):print("LAN Switch ON") cmd="sudo ifconfig eth0 169.254.69.69" direct=True else: return direct if(DEBUG==False):os.system(cmd) if(SNDON):os.system("hostname -I | flite") return direct Add another function, flashled(), which will just toggle the state of the LED each time it is called. The code for this function is as follows: def flashled(ledon): if ledon: ledon=False else: ledon=True GPIO.output(LED,ledon) return ledon Finally, we adjust the main loop to also call doChangeLAN() and use the result to decide whether we call flashled() using ledon to keep track of the LED's previous state each time. The main() function should now be updated as follows: def main(): gpio_setup() GPIO.output(LED,1) directlan=False ledon=True while True: if(DEBUG):print("Waiting for >3sec button press") if GPIO.input(SHTDWN_BTN)==False: doShutdown() directlan= doChangeLAN(directlan) if directlan: flashled(ledon) time.sleep(1) 196 Chapter 6 The GPIO keypad input We have seen how we can monitor inputs on the GPIO to launch applications and control the Raspberry Pi; however, sometimes we need to control third-party programs. Using the uInput library, we can emulate key presses from a keyboard (or even mouse movement) to control any program, using our own custom hardware. For more information about using uInput, visit http://tjjr.fi/sw/python-uinput/. Getting ready Perform the following steps to install uInput: 1. First, we need to download uInput. You will need to download the uInput Python library from Github (~50 KB) using the following commands: wget https://github.com/tuomasjjrasanen/python-uinput/archive/ master.zip unzip master.zip The library will unzip to a directory called python-uinput-master. Once completed, you can remove the ZIP file using the following command: rm master.zip 2. Install the required packages (if you have installed them already, the apt-get command will ignore them) using the following commands: sudo apt-get install python3-setuptools python3-dev sudo apt-get install libudev-dev 3. Compile and install uInput using the following commands: cd python-uinput-master sudo python3 setup.py install 4. Finally, we load the new uinput kernel module using the following command: sudo modprobe uinput To ensure it is loaded on startup, we can add uinput to the modules file using the following command: sudo nano /etc/modules Put uinput on a new line in the file and save it (Ctrl + X,Y). 197 Using Python to Drive Hardware 5. Create the following circuit using the following equipment: Breadboard (half-sized or larger) 7 x Dupont female to male patch wires Six push buttons 6 x 470-ohm resistors Breadboarding wire (solid core) GPIO keypad circuit layout The keypad circuit can also be built into a permanent circuit by soldering the components into a Vero prototype board (also known as a stripboard), as shown in the following image: GPIO keypad Pi hardware module 198 Chapter 6 This circuit is available as a solder-yourself kit from PiHardware.com. 6. Connect the circuit to the Raspberry Pi GPIO pins as follows: Button GND GPIO pin 6 v B_DOWN 22 < B_LEFT 18 ^ B_UP 15 > B_RIGHT 13 1 B_1 11 2 B_2 7 How to do it… Create a gpiokeys.py script as follows: #!/usr/bin/python3 #gpiokeys.py import time import RPi.GPIO as GPIO import uinput #HARDWARE SETUP # GPIO # 2[==G=====<=V==]26[=======]40 # 1[===2=1>^=====]25[=======]39 B_DOWN = 22 #V B_LEFT = 18 #< B_UP = 15 #^ B_RIGHT = 13 #> B_1 = 11 #1 B_2 = 7 #2 DEBUG=True BTN = [B_UP,B_DOWN,B_LEFT,B_RIGHT,B_1,B_2] 199 Using Python to Drive Hardware MSG = ["UP","DOWN","LEFT","RIGHT","1","2"] #Setup the DPad module pins and pull-ups def dpad_setup(): #Set up the wiring GPIO.setmode(GPIO.BOARD) # Setup BTN Ports as INPUTS for val in BTN: # set up GPIO input with pull-up control #(pull_up_down can be: # PUD_OFF, PUD_UP or PUD_DOWN, default PUD_OFF) GPIO.setup(val, GPIO.IN, pull_up_down=GPIO.PUD_UP) def main(): #Setup uinput events = (uinput.KEY_UP,uinput.KEY_DOWN,uinput.KEY_LEFT, uinput.KEY_RIGHT,uinput.KEY_ENTER,uinput.KEY_ENTER) device = uinput.Device(events) time.sleep(2) # seconds dpad_setup() print("DPad Ready!") btn_state=[False,False,False,False,False,False] key_state=[False,False,False,False,False,False] while True: #Catch all the buttons pressed before pressing the related keys for idx, val in enumerate(BTN): if GPIO.input(val) == False: btn_state[idx]=True else: btn_state[idx]=False #Perform the button presses/releases (but only change state once) for idx, val in enumerate(btn_state): if val == True and key_state[idx] == False: if DEBUG:print (str(val) + ":" + MSG[idx]) device.emit(events[idx], 1) # Press. key_state[idx]=True elif val == False and key_state[idx] == True: if DEBUG:print (str(val) + ":!" + MSG[idx]) 200 Chapter 6 device.emit(events[idx], 0) # Release. key_state[idx]=False time.sleep(.1) try: main() finally: GPIO.cleanup() #End How it works… First, we import uinput and define the wiring of the keypad buttons. For each of the buttons in BTN, we enable them as inputs with internal pull-ups enabled. Next, we set up uinput, defining the keys we want to emulate and adding them to the uinput.Device() function. We wait a few seconds to allow uinput to initialize, set the initial button and key states, and start our main loop. The main loop is split into two sections: the first part checks through the buttons and records the states in btn_state, and the second part compares the btn_state with the current key_state array. This way, we can detect a change in btn_state and call device.emit() to toggle the state of the key. To allow us to run this script in the background, we can run it with & as shown in the following command: sudo python3 gpiokeys.py & The & character allows the command to run in the background, so we can continue with the command line to run other programs. You can use fg to bring it back to the foreground, or %1, %2, and so on if you have several commands running. Use jobs to get a list. You can even put a process/program on hold to get to the command prompt by pressing Ctrl + Z and then resume it with bg (which will let it run in the background). You can test the keys using the game created in the Creating an overhead scrolling game recipe in Chapter 4, Creating Games and Graphics, which you can now control using your GPIO directional pad. Don't forget that if you are connecting to the Raspberry Pi remotely, any key presses will only be active on the locally connected screen. 201 Using Python to Drive Hardware There's more… We can do more using uinput to provide hardware control for other programs, including those that require mouse input. Generating other key combinations You can create several different key mappings in your file to support different programs. For instance, the events_z80 key mapping would be useful for a Spectrum Emulator such as fuze (browse to http://raspi.tv/2012/how-to-install-fuse-zx-spectrumemulator-on-raspberry-pi for details). The events_omx key mappings are suitable for controlling video played through the OMX Player using the following command: omxplayer filename.mp4 You can get a list of keys supported by omxplayer by using the -k parameter. Replace the line that defines the events list with a new key mapping, and select different ones by assigning them to events using the following code: events_dpad = (uinput.KEY_UP,uinput.KEY_DOWN,uinput.KEY_LEFT, uinput.KEY_RIGHT,uinput.KEY_ENTER,uinput.KEY_ENTER) events_z80 = (uinput.KEY_Q,uinput.KEY_A,uinput.KEY_O, uinput.KEY_P,uinput.KEY_M,uinput.KEY_ENTER) events_omx = (uinput.KEY_EQUAL,uinput.KEY_MINUS,uinput.KEY_LEFT, uinput.KEY_RIGHT,uinput.KEY_P,uinput.KEY_Q) You can find all the KEY definitions in the input.h file; you can view it using the less command (press Q to exit) as shown in the following command: less /usr/include/linux/input.h Emulating mouse events The uinput library can emulate mouse and joystick events as well as keyboard presses. To use the buttons to simulate a mouse, we can adjust the script to use mouse events (as well as defining mousemove to set the step size of the movement) using the following code: MSG = ["M_UP","M_DOWN","M_LEFT","M_RIGHT","1","Enter"] events_mouse=(uinput.REL_Y,uinput.REL_Y, uinput.REL_X, uinput.REL_X,uinput.BTN_LEFT,uinput.BTN_RIGHT) mousemove=1 202 Chapter 6 We also need to modify the button handling to provide continuous movement, as we don't need to keep track of the state of the keys for the mouse. To do so, use the following code: #Perform the button presses/releases #(but only change state once) for idx, val in enumerate(btn_state): if MSG[idx] == "M_UP" or MSG[idx] == "M_LEFT": state = -mousemove else: state = mousemove if val == True: device.emit(events[idx], state) # Press. elif val == False: device.emit(events[idx], 0) # Release. time.sleep(0.01) Multiplexed color LEDs The next example in this chapter demonstrates that some seemingly simple hardware can produce some impressive results if controlled with software. We return to using some RGB LEDs that are wired so that we only need to use eight GPIO pins to control the red, green, and blue elements of five RGB LEDs using a method called hardware multiplexing (see the Hardware multiplexing subsection in the There's more… section of this recipe). 203 Using Python to Drive Hardware Getting ready You will need the RGB LED module shown in the following image: The RGB LED module from PiHardware.com As you can see in the preceding image, the RGB LED module from PiHardware.com comes with GPIO pins and a Dupont female to female cable for connecting it. Although there are two sets of pins labelled 1 to 5, only one side needs to be connected. 204 Chapter 6 Alternatively, you can recreate your own with the following circuit using five common cathode RGB LEDs, 3 x 470-ohm resistors, and a Vero prototype board (or large breadboard). The circuit will look as shown in the following diagram: Circuit diagram for the RGB LED module Strictly speaking, we should use 15 resistors in this circuit (one for each RGB LED element), which will avoid interference from LEDs sharing the same resistor and would also prolong the life of the LEDs themselves if switched on together. However, there is only a slight advantage, particularly since we intend to drive each RGB LED independently of the other four to achieve multicolor effects. You will need to connect the circuit to the Raspberry Pi GPIO header as follows: RGB LED 2 3 Rpi GPIO Pin 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 Rpi GPIO Pin 1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39 R G B RGB LED 1 5 4 205 Using Python to Drive Hardware How to do it… Create the rgbled.py script and perform the following steps: 1. Import all the required modules and define values to be used with the help of the following code: #!/usr/bin/python3 #rgbled.py import time import RPi.GPIO as GPIO #Setup Active states #Common Cathode RGB-LEDs (Cathode=Active Low) LED_ENABLE = 0; LED_DISABLE = 1 RGB_ENABLE = 1; RGB_DISABLE = 0 #HARDWARE SETUP # GPIO # 2[=====1=23=4==]26[=======]40 # 1[===5=RGB=====]25[=======]39 #LED CONFIG - Set GPIO Ports LED1 = 12; LED2 = 16; LED3 = 18; LED4 = 22; LED5 = 7 LED = [LED1,LED2,LED3,LED4,LED5] RGB_RED = 11; RGB_GREEN = 13; RGB_BLUE = 15 RGB = [RGB_RED,RGB_GREEN,RGB_BLUE] #Mixed Colors RGB_CYAN = [RGB_GREEN,RGB_BLUE] RGB_MAGENTA = [RGB_RED,RGB_BLUE] RGB_YELLOW = [RGB_RED,RGB_GREEN] RGB_WHITE = [RGB_RED,RGB_GREEN,RGB_BLUE] RGB_LIST = [RGB_RED,RGB_GREEN,RGB_BLUE,RGB_CYAN, RGB_MAGENTA,RGB_YELLOW,RGB_WHITE] 2. Define functions to set up the GPIO pins using the following code: def led_setup(): '''Setup the RGB-LED module pins and state.''' #Set up the wiring GPIO.setmode(GPIO.BOARD) # Setup Ports for val in LED: GPIO.setup(val, GPIO.OUT) for val in RGB: GPIO.setup(val, GPIO.OUT) led_clear() 206 Chapter 6 3. Define our utility functions to help control the LEDs using the following code: def led_gpiocontrol(pins,state): '''This function will control the state of a single or multiple pins in a list.''' #determine if "pins" is a single integer or not if isinstance(pins,int): #Single integer - reference directly GPIO.output(pins,state) else: #if not, then cycle through the "pins" list for i in pins: GPIO.output(i,state) def led_activate(led,color): '''Enable the selected led(s) and set the required color(s) Will accept single or multiple values''' #Enable led led_gpiocontrol(led,LED_ENABLE) #Enable color led_gpiocontrol(color,RGB_ENABLE) def led_deactivate(led,color): '''Deactivate the selected led(s) and set the required color(s) will accept single or multiple values''' #Disable led led_gpiocontrol(led,LED_DISABLE) #Disable color led_gpiocontrol(color,RGB_DISABLE) def led_time(led, color, timeon): '''Switch on the led and color for the timeon period''' led_activate(led,color) time.sleep(timeon) led_deactivate(led,color) def led_clear(): '''Set the pins to default state.''' for val in LED: GPIO.output(val, LED_DISABLE) for val in RGB: GPIO.output(val, RGB_DISABLE) def led_cleanup(): 207 Using Python to Drive Hardware '''Reset pins to default state and release GPIO''' led_clear() GPIO.cleanup() 4. Create a test function to demonstrate the functionality of the module: def main(): '''Directly run test function. This function will run if the file is executed directly''' led_setup() led_time(LED1,RGB_RED,5) led_time(LED2,RGB_GREEN,5) led_time(LED3,RGB_BLUE,5) led_time(LED,RGB_MAGENTA,2) led_time(LED,RGB_YELLOW,2) led_time(LED,RGB_CYAN,2) if __name__=='__main__': try: main() finally: led_cleanup() #End How it works… To start with, we define the hardware setup by defining the states required to Enable and Disable the LED depending on the type of RGB LED (common cathode) used. If you are using a common anode device, just reverse the Enable/Disable states. Next, we define the GPIO mapping to the pins to match the wiring we did previously. We also define some basic color combinations by combining red, green, and/or blue together, as shown in the following diagram: LED color combinations We define a series of useful functions, the first being led_setup(), which will set the GPIO numbering to GPIO.BOARD and define all the pins used to be outputs. We also call a function named led_clear(), which will set the pins to the default state with all the pins disabled. 208 Chapter 6 This means the LED pins, 1-5 (the common cathode on each LED), are set to HIGH, while the RGB pins (the separate anodes for each color) are set to LOW. We create a function called led_gpiocontrol() that will allow us to set the state of one or more pins. The isinstance() function allows us to test a value to see whether it matches a particular type (in this case, a single integer); then we can either set the state of that single pin or iterate through the list of pins and set each one. Next, we define two functions, led_activate() and led_deactivate(), which will enable and disable the specified LED and color. Finally, we define led_time(), which will allow us to specify an LED, color, and time to switch it on for. We also create led_cleanup() to reset the pins (and LEDs) to the default values and call GPIO.cleanup() to release the GPIO pins in use. This script is intended to become a library file, so we will use the if __name__=='__ main__' check to only run our test code when running the file directly: By checking the value of __name__, we can determine whether the file was run directly (it will equal __main__) or whether it was imported by another Python script. This allows us to define special test code that is only executed when we directly load and run the file. If we include this file as a module in another script, then this code will not be executed. We have used this technique previously in the There's more… section in the Working with text and strings recipe of Chapter 2, Starting with Python Strings, Files, and Menus. As before, we will use try/finally to allow us to always perform cleanup actions, even if we exit early. To test the script, we will set the LEDs to light up in various colors one after another. There's more… We can create a few different colors by switching on one or more parts of the RGB LED at a time. However, with some clever programming, we can create a whole spectrum of colors. Also, we can display different colors on each LED, seemingly at the same time. 209 Using Python to Drive Hardware Hardware multiplexing An LED requires a high voltage on the anode side and a lower voltage on the cathode side to light up. The RGB LEDs used in the circuit are common cathode, so we must apply a high voltage (3.3V) on the RGB pins and a low voltage (0V) on the cathode pin (wired to pins 1 to 5 for each of the LEDs). The cathode and RGB pin states are as follows: Cathode and RGB pin states Therefore, we can enable one or more of the RGB pins but still control which of the LEDs are lit. We enable the pins of the LEDs we want to light up and disable the ones we don't. This allows us to use far fewer pins than we would need to control each of the 15 RGB lines separately. Displaying random patterns We can add new functions to our library to produce different effects, such as generating random colors. The following function uses randint() to get a value between 1 and the number of colors. We ignore any values that are over the number of the available colors so that we can control how often the LEDs may be switched off. Perform the following steps to add the required functions: 1. Add the randint() function from the random module to the rgbled.py script using the following code: from random import randint 2. Now add led_rgbrandom() using the following code: def led_rgbrandom(led,period,colors): ''' Light up the selected led, for period in seconds, in one of the possible colors. The colors can be 1 to 3 for RGB, or 1-6 for RGB plus combinations, 1-7 includes white. Anything over 7 will be set as OFF (larger the number more chance of OFF).''' 210 Chapter 6 value = randint(1,colors) if value < len(RGB_LIST): led_time(led,RGB_LIST[value-1],period) 3. Use the following commands in the main() function to create a series of flashing LEDs: for i in range(20): for j in LED: #Select from all, plus OFF led_rgbrandom(j,0.1,20) Mixing multiple colors Until now, we have only displayed a single color at a time on one or more of the LEDs. If you consider how the circuit is wired up, you might wonder how can we get one LED to display one color and another a different one at the same time? The simple answer is that we don't need to—we just do it quickly! All we need to do is display one color at a time but change it very quickly back and forth, so fast the color looks like a mix of the two (or even a combination of the three red/green/blue LEDs). Fortunately, this is something that computers such as the Raspberry Pi can do very easily, even allowing us to combine the RGB elements to make multiple shades of colors across all five LEDs. Perform the following steps to mix the colors: 1. Add combo color definitions to the top of the rgbled.py script, after the definition of the mixed colors, using the following code: #Combo Colors RGB_AQUA = [RGB_CYAN,RGB_GREEN] RGB_LBLUE = [RGB_CYAN,RGB_BLUE] RGB_PINK = [RGB_MAGENTA,RGB_RED] RGB_PURPLE = [RGB_MAGENTA,RGB_BLUE] RGB_ORANGE = [RGB_YELLOW,RGB_RED] RGB_LIME = [RGB_YELLOW,RGB_GREEN] RGB_COLORS = [RGB_LIME,RGB_YELLOW,RGB_ORANGE,RGB_RED, RGB_PINK,RGB_MAGENTA,RGB_PURPLE,RGB_BLUE, RGB_LBLUE,RGB_CYAN,RGB_AQUA,RGB_GREEN] The preceding code will provide the combination of colors to create our shades, with RGB_COLORS providing a smooth progression through the shades. 2. Next, we need to create a function called led_combo() to handle single or multiple colors. The code for the function will be as follows: def led_combo(pins,colors,period): #determine if "colors" is a single integer or not if isinstance(colors,int): #Single integer - reference directly 211 Using Python to Drive Hardware led_time(pins,colors,period) else: #if not, then cycle through the "colors" list for i in colors: led_time(pins,i,period) 3. Now we can create a new script, rgbledrainbow.py, to make use of the new functions in our rgbled.py module. The rgbledrainbow.py script will be as follows: #!/usr/bin/python3 #rgbledrainbow.py import time import rgbled as RGBLED def next_value(number,max): number = number % max return number def main(): print ("Setup the RGB module") RGBLED.led_setup() # Multiple LEDs with different Colors print ("Switch on Rainbow") led_num = 0 col_num = 0 for l in range(5): print ("Cycle LEDs") for k in range(100): #Set the starting point for the next set of colors col_num = next_value(col_num+1,len(RGBLED.RGB_COLORS)) for i in range(20): #cycle time for j in range(5): #led cycle led_num = next_value(j,len(RGBLED.LED)) led_color = next_value(col_num+led_num, len(RGBLED.RGB_COLORS)) RGBLED.led_combo(RGBLED.LED[led_num], RGBLED.RGB_COLORS[led_color],0.001) print ("Cycle COLORs") for k in range(100): #Set the next color col_num = next_value(col_num+1,len(RGBLED.RGB_COLORS)) for i in range(20): #cycle time 212 Chapter 6 for j in range(5): #led cycle led_num = next_value(j,len(RGBLED.LED)) RGBLED.led_combo(RGBLED.LED[led_num], RGBLED.RGB_COLORS[col_num],0.001) print ("Finished") if __name__=='__main__': try: main() finally: RGBLED.led_cleanup() #End The main() function will first cycle through the LEDs, setting each color from the RGB_COLORS array on all the LEDs. Then, it will cycle through the colors, creating a rainbow effect over the LEDs: Cycle through multiple colors on the five RGB LEDs 213 Using Python to Drive Hardware Writing messages using Persistence of Vision Persistence of Vision (POV) displays can produce an almost magical effect, displaying images in the air by moving a line of LEDs back and forth very quickly or around in circles. The effect works because your eyes are unable to adjust fast enough to separate out the individual flashes of light, and so you observe a merged image (the message or picture being displayed). Persistence of vision using RGB LEDs Getting ready This recipe also uses the RGB LED kit used in the previous recipe; you will also need the following additional items: ff Breadboard (half-sized or larger) ff 2 x Dupont female to male patch wires ff Tilt switch (ball-bearing type is suitable) ff 1 x 470 ohm resistor (R_Protect) ff Breadboarding wire (solid core) 214 Chapter 6 The tilt switch should be added to the RGB LED (as described in the Getting ready section of the Multiplexed color LEDs recipe). The tilt switch is wired as follows: Tilt switch is connected to GPIO Input (GPIO pin 24) and Gnd (GPIO pin 6) 215 Using Python to Drive Hardware To reproduce the POV image, you will need to be able to quickly move the LEDs and tilt switch back and forth. Note how the tilt switch is mounted angled to the side, so the switch will open when moved to the left. It is recommended that the hardware is mounted onto a length of wood or similar. You can even use a portable USB battery pack along with a Wi-Fi dongle to power and control the Raspberry Pi through a remote connection (see Chapter 1, Getting Started with a Raspberry Pi – Connecting Remotely to the Raspberry Pi over the Network using SSH (and X11 Forwarding), for details): Persistence of Vision hardware setup You will also need the completed rgbled.py file, which we will extend further in the How to do it… section. How to do it… Create a script called tilt.py to report the state of the tilt switch: #!/usr/bin/python3 #tilt.py import RPi.GPIO as GPIO #HARDWARE SETUP # GPIO # 2[===========T=]26[=======]40 # 1[=============]25[=======]39 #Tilt Config TILT_SW = 24 def tilt_setup(): #Setup the wiring GPIO.setmode(GPIO.BOARD) #Setup Ports GPIO.setup(TILT_SW,GPIO.IN,pull_up_down=GPIO.PUD_UP) def tilt_moving(): 216 Chapter 6 #Report the state of the Tilt Switch return GPIO.input(TILT_SW) def main(): import time tilt_setup() while True: print("TILT %s"% (GPIO.input(TILT_SW))) time.sleep(0.1) if __name__=='__main__': try: main() finally: GPIO.cleanup() print("Closed Everything. END") #End You can test the script by running it directly with the following command: sudo python3 tilt.py Add the following rgbled_pov() function to the rgbled.py script we created previously; this will allow us to display a single line of our image: def rgbled_pov(led_pattern,color,ontime): '''Disable all the LEDs and re-enable the LED pattern in the required color''' led_deactivate(LED,RGB) for led_num,col_num in enumerate(led_pattern): if col_num >= 1: led_activate(LED[led_num],color) time.sleep(ontime) We will now create the following file, called rgbledmessage.py, to perform the required actions to display our message. First we will import the modules used, the updated rgbled module, the new tilt module, and the python os module. Initially, we set DEBUG to True so the Python terminal will display additional information while the script is running: #!/usr/bin/python3 # rgbledmessage.py import rgbled as RGBLED import tilt as TILT import os DEBUG = True 217 Using Python to Drive Hardware Add a readMessageFile() function to read the content of the letters.txt file and then add processFileContent() to generate a Python dictionary of the LED patterns for each letter: def readMessageFile(filename): assert os.path.exists(filename), 'Cannot find the message file: %s' % (filename) try: with open(filename, 'r') as theFile: fileContent = theFile.readlines() except IOError: print("Unable to open %s" % (filename)) if DEBUG:print ("File Content START:") if DEBUG:print (fileContent) if DEBUG:print ("File Content END") dictionary = processFileContent(fileContent) return dictionary def processFileContent(content): letterIndex = [] #Will contain a list of letters stored in the file letterList = [] #Will contain a list of letter formats letterFormat = [] #Will contain the format of each letter firstLetter = True nextLetter = False LETTERDIC={} #Process each line that was in the file for line in content: # Ignore the # as comments if '#' in line: if DEBUG:print ("Comment: %s"%line) #Check for " in the line = index name elif '"' in line: nextLetter = True line = line.replace('"','') #Remove " characters LETTER=line.rstrip() if DEBUG:print ("Index: %s"%line) #Remaining lines are formatting codes else: #Skip firstLetter until complete if firstLetter: firstLetter = False nextLetter = False lastLetter = LETTER #Move to next letter if needed if nextLetter: 218 Chapter 6 nextLetter = False LETTERDIC[lastLetter]=letterFormat[:] letterFormat[:] = [] lastLetter = LETTER #Save the format data values = line.rstrip().split(' ') row = [] for val in values: row.append(int(val)) letterFormat.append(row) LETTERDIC[lastLetter]=letterFormat[:] #Show letter patterns for debugging if DEBUG:print ("LETTERDIC: %s" %LETTERDIC) if DEBUG:print ("C: %s"%LETTERDIC['C']) if DEBUG:print ("O: %s"%LETTERDIC['O']) return LETTERDIC Add a createBuffer() function, which will convert a message into a series of LED patterns for each letter (assuming the letter is defined by the letters.txt file): def createBuffer(message,dictionary): buffer=[] for letter in message: try: letterPattern=dictionary[letter] except KeyError: if DEBUG:print("Unknown letter %s: use _"%letter) letterPattern=dictionary['_'] buffer=addLetter(letterPattern,buffer) if DEBUG:print("Buffer: %s"%buffer) return buffer def addLetter(letter,buffer): for row in letter: buffer.append(row) buffer.append([0,0,0,0,0]) buffer.append([0,0,0,0,0]) return buffer Next, we define displayBuffer() to display the LED patterns using the rgbled_pov() function in the rgbled module: def displayBuffer(buffer): position=0 while(1): 219 Using Python to Drive Hardware if(TILT.tilt_moving()==False): position=0 elif (position+1) This will open a SQLite console within which we enter SQL commands directly. For example, the following commands will create a new table, add some data, display the content, and then remove the table: CREATE TABLE mytable (info TEXT, info2 TEXT,); INSERT INTO mytable VALUES ("John","Smith"); INSERT INTO mytable VALUES ("Mary","Jane"); John|Smith Mary|Jane DROP TABLE mytable; .exit You will need the same hardware setup as the previous recipe, as detailed in the Getting ready section of the Using devices with the I2C bus recipe. How to do it... Create the following script, mysqlite_adc.py: #!/usr/bin/python3 #mysql_adc.py import sqlite3 import datetime import data_adc as dataDevice import time import os DEBUG=True SHOWSQL=True CLEARDATA=False VAL0=0;VAL1=1;VAL2=2;VAL3=3 #Set data order FORMATBODY="%5s %8s %14s %12s %16s" FORMATLIST="%5s %12s %10s %16s %7s" 260 Chapter 7 DATEBASE_DIR="/var/databases/datasite/" DATEBASE=DATEBASE_DIR+"mydatabase.db" TABLE="recordeddata" DELAY=1 #approximate seconds between samples def captureSamples(cursor): if(CLEARDATA):cursor.execute("DELETE FROM %s" %(TABLE)) myData = dataDevice.device() myDataNames=myData.getName() if(DEBUG):print(FORMATBODY%("##",myDataNames[VAL0], myDataNames[VAL1],myDataNames[VAL2], myDataNames[VAL3])) for x in range(10): data=myData.getNew() for i,dataName in enumerate(myDataNames): sqlquery = "INSERT INTO %s (itm_name, itm_value) " %(TABLE) + \ "VALUES('%s', %s)" \ %(str(dataName),str(data[i])) if (SHOWSQL):print(sqlquery) cursor.execute(sqlquery) if(DEBUG):print(FORMATBODY%(x, data[VAL0],data[VAL1], data[VAL2],data[VAL3])) time.sleep(DELAY) cursor.commit() def displayAll(connect): sqlquery="SELECT * FROM %s" %(TABLE) if (SHOWSQL):print(sqlquery) cursor = connect.execute (sqlquery) print(FORMATLIST%("","Date","Time","Name","Value")) for x,column in enumerate(cursor.fetchall()): print(FORMATLIST%(x,str(column[0]),str(column[1]), str(column[2]),str(column[3]))) def createTable(cursor): print("Create a new table: %s" %(TABLE)) sqlquery="CREATE TABLE %s (" %(TABLE) + \ "itm_date DEFAULT (date('now','localtime')), " + \ 261 Sense and Display Real-World Data "itm_time DEFAULT (time('now','localtime')), " + \ "itm_name, itm_value)" if (SHOWSQL):print(sqlquery) cursor.execute(sqlquery) cursor.commit() def openTable(cursor): try: displayAll(cursor) except sqlite3.OperationalError: print("Table does not exist in database") createTable(cursor) finally: captureSamples(cursor) displayAll(cursor) try: if not os.path.exists(DATEBASE_DIR): os.makedirs(DATEBASE_DIR) connection = sqlite3.connect(DATEBASE) try: openTable(connection) finally: connection.close() except sqlite3.OperationalError: print("Unable to open Database") finally: print("Done") #End If you do not have the ADC module hardware, you can capture local data by setting the dataDevice module as data_local. Ensure you have data_local.py (from the There's more… section in the Reading analog data using an analog-to-digital converter recipe) in the same directory as this script: import data_local as dataDevice This will capture the local data (RAM, CPU activity, temperature, and so on) to the SQLite database instead of ADC samples. 262 Chapter 7 How it works... When the script is first run, it will create a new SQLite database file called mydatabase.db, which will add a table named recordeddata. The table is generated by createTable(), which runs the following SQLite command: CREATE TABLE recordeddata ( itm_date DEFAULT (date('now','localtime')), itm_time DEFAULT (time('now','localtime')), itm_name, itm_value ) The new table will contain the following data items: Name itm_date Description itm_time Used to store the time of the data sample. When the data record is created the current time (using time('now','localtime')) is applied as the DEFAULT value. itm_name Used to record the name of the sample. itm_value Used to keep the sampled value. Used to store the date of the data sample. When the data record is created the current date (using date('now','localtime')) is applied as the DEFAULT value. We then use the same the method to capture ten data samples from the ADC as we did in the Logging and plotting data recipe previously (as shown in the function captureSamples()). However, this time we will then add the captured data into our new SQLite database table, using the following SQL command (applied using cursor.execute(sqlquery)): INSERT INTO recordeddata (itm_name, itm_value) VALUES ('0:Light', 210) 263 Sense and Display Real-World Data The current date and time will be added by default to each record as it is created. We end up with a set of 40 records (four records for every cycle of ADC samples captured), which are now stored in the SQLite database. Eight ADC samples have been captured and stored in the SQLite database After the records have been created we must remember to call cursor.commit(), which will save all the new records to the database. The last part of the script calls displayAll(), which will use the following SQL command: SELECT * FROM recordeddata This will select all of the data records in the recordeddata table, and we use cursor. fetch() to provide the selected data as a list we can iterate through: for x,column in enumerate(cursor.fetchall()): print(FORMATLIST%(x,str(column[0]),str(column[1]), str(column[2]),str(column[3]))) This allows us to print out the full contents of the database, displaying the captured data. 264 Chapter 7 Notice that here we use the try, except, and finally constructs in this script to attempt to handle the mostly likely scenario users will face when running the script. First we ensure that if the database directory doesn't exist, we create it. Next we try opening the database file; this process will automatically create a new database file if one doesn't already exist. If either of these initial steps fail (due to not having read/write permissions, for example) we cannot continue so we report that we cannot open the database and simply exit the script. Next, we try to open the required table within the database and display it; if the database file is brand new this operation will always fail as it will be empty. However, if this occurs we just catch the exception and create the table before continuing with the script to add our sampled data to the table and display it. This allows the script to gracefully handle potential problems, take corrective action and then continue smoothly. The next time the script is run, the database and table will already exist, so we won't need to create them a second time and we can append the sample data to the table within the same database file. There's more… There are many variants of SQL servers available (such as MySQL, Microsoft SQL Server, or PostgreSQL); however, they should at least have the following primary commands (or equivalent): CREATE, INSERT, SELECT, WHERE, UPDATE, SET, DELETE, and DROP You should find that even if you choose to use a different SQL server to the SQLite one used here, the SQL commands will be relatively similar. CREATE The CREATE TABLE command is used to define a new table by specifying the column names (and also to set DEFAULT values if desired): CREATE TABLE table_name ( column_name1 TEXT, column_name2 INTEGER DEFAULT 0, column_name3 REAL ) The previous SQL command will create a new table called table_name, containing three data items. One column would contain text, another integers (for example, 1, 3, -9) and finally one column for real numbers (for example, 5.6, 3.1749, 1.0). 265 Sense and Display Real-World Data INSERT The INSERT command will add a particular entry to a table in the database: INSERT INTO table_name (column_name1name1, column_name2name2, column_ name3)name3) VALUES ('Terry'Terry Pratchett', 6666, 27.082015)082015) This will enter the values provided into the corresponding columns in the table. SELECT The SELECT command allows us to specify a particular column or columns from the database table, returning a list of records with the data: SELECT column_name1, column_name2 FROM table_name Or to select all items, use this command: SELECT * FROM table_name WHERE The WHERE command is used to specify specific entries to be selected, updated, or deleted: SELECT * FROM table_name WHERE column_name1= 'Terry Pratchett' This will SELECT any records where the column_name1 matches 'Terry Pratchett'. UPDATE The UPDATE command will allow us to change (SET) the values of data in each of the specified columns. We can also combine this with the WHERE command to limit the records the change is applied to: UPDATE table_name SET column_name2=49name2=49,column_name3=30name3=30.111997 WHERE column_name1name1= 'Douglas Adams'Adams'; DELETE The DELETE command allows any records selected using WHERE to be removed from the specified table. However, if the whole table is selected, using DELETE * FROM table_name will delete the entire contents of the table: DELETE FROM table_name WHERE columncolumn_name2=9999 266 Chapter 7 DROP The DROP command allows a table to be removed completely from the database: DROP table_name Be warned that this will permanently remove all the data that was stored in the specified table and the structure. Viewing data from your own webserver Gathering and collecting information into databases is very helpful, but if it is locked inside a database or a file it isn't much use. However, if we allow the stored data to be viewed via a web page it will be far more accessible; not only can we view the data from other devices, we can also share it with others on the same network. We shall create a local web server to query and display the captured SQLite data and allow it to be viewed through a PHP web interface. This will allow the data to be viewed, not only via the web browser on the Raspberry, Pi but also on other devices, such as cell phones or tablets, on the local network: Data captured in the SQLite database displayed via a web-page 267 Sense and Display Real-World Data Using a web server to enter and display information is a powerful way to allow a wide range of users to interact with your projects. The following example demonstrates a web server setup that can be customized for a variety of uses. Getting ready Ensure you have completed the previous recipe so that the sensor data has been collected and stored in the SQLite database. We need to install a web server (Apache2) and enable PHP support to allow SQLite access. Use these commands to install a web server and PHP: sudo apt-get update sudo aptitude install apache2 php5 php5-sqlite The /var/www/ directory is used by the web server; by default it will load index.html (or index.php), otherwise it will just display a list of the links to the files within the directory. To test the web server is running, create a default index.html page. To do this you will need to create the file using sudo permissions (the /var/www/ directory is protected from changes made by normal users). Use the following command: sudo nano /var/www/index.html Create index.html with the following content: It works!
Close and save the file (using Ctrl + X, Y and Enter). If you are using the Raspberry Pi with a screen, you can check it is working by loading the desktop: startx Then, open the web browser (epiphany-browser) and enter http://localhost as the address. You should see the following test page, indicating the web server is active: Raspberry Pi browser displaying the test page, located at http://localhost 268 Chapter 7 If you are using the Raspberry Pi remotely or it is connected to your network, you should also be able to view the page on another computer on your network. First, identify the IP address of the Raspberry Pi (using sudo hostname -I) and then use this as the address in your web browser. You may even find you can use the actual hostname of the Raspberry Pi (by default this is http://raspberrypi/). If you are unable to see the web page from another computer, ensure that you do not have a firewall enabled (on the computer itself or on your router) that could be blocking it. Next, we can test that PHP is operating correctly. We can create the following web page, test.php, and ensure it is located in the /var/www/ directory: ; View the test.php page at the following location: http://localhost/test.php We are ready to write our own PHP web page to view the data in the SQLite database. 269 Sense and Display Real-World Data How to do it... Create the following PHP files and save them in the webserver directory, /var/www/./. Use the following command to create the PHP file: sudo nano /var/www/show_data_lite.php The show_data_lite.php file should contain:DatabaseDatabase Data Press button to remove the table data
Recorded Data
query($strSQL); //Loop through the response while($column = $response->fetch()) { //Display the content of the response echo $column[0] . " "; echo $column[1] . " "; echo $column[2] . " "; echo $column[3] . "
"; } ?> Done