1. Trang chủ
  2. » Công Nghệ Thông Tin

Practical Arduino Cool Projects for Open Source Hardware- P28 docx

10 218 0

Đang tải... (xem toàn văn)

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 10
Dung lượng 173,77 KB

Nội dung

CHAPTER 13  WEATHER STATION RECEIVER To increase the sensitivity of the receiver, it needs an external antenna. Radio antennas need to be “tuned” to the correct wavelength for the frequency you are listening for, and the wavelength of a 433MHz signal is approximately 69cm or 27in. A good tuned length to go for in this case is 1/2 wavelength, which is 34.5cm or 13.5in. We need to allow a little distance for the length of the pin and track, so we’ll create an antenna just slightly shorter than half-wave. Cut a piece of lightweight hookup wire about 33cm (13in) long and strip back a few millimeters from one end, then tin it with solder so it’s ready to connect to the board. On the prototype we soldered the end of the antenna to a female header so it could be easily removed from the board. Having a 33cm piece of wire hanging loose might not be very convenient, so to keep things neat you can wrap it around a nonconductive former so that it forms a widely spaced coil. Our prototype antenna is wrapped around a piece cut from the body of a ballpoint pen and held in place with tape, but if you wanted to make it look a bit neater you could slip a length of heat-shrink tubing over the top to hold everything in place (see Figure 13-11). Figure 13-11. Antenna wrapped around body of ballpoint pen You should now have a fully assembled receiver shield ready to go, so plug it into your Arduino and connect it to your computer via USB (see Figure 13-12). 249 CHAPTER 13  WEATHER STATION RECEIVER Figure 13-12. Arduino, receiver, and antenna assembled and ready for use Weather Station Receiver Program The program for this project is one of the more complex ones in the book and uses a number of things you might not have seen in Arduino sketches before. It’s fairly long, so you definitely shouldn’t bother trying to type it all in: just download it from the project page on the Practical Arduino web site at www.practicalarduino.com/projects/weather-station-receiver. Unlike many Arduino programs it’s not all contained within a single .pde file, but is instead split into two files: WeatherStationReceiver.pde for the main program and WeatherStationReceiver.h for some general declarations. Splitting code across multiple files is standard practice in larger software projects and provides a number of major benefits, including easier navigation in a code editor, conceptual encapsulation of sections of the project, easier change tracking in source code management systems, and less problems when multiple people work on the codebase simultaneously. Many larger software projects have their code split up across dozens, hundreds, or even thousands of files. A tiny microcontroller such as an Arduino simply doesn’t have the memory to run very large programs, but even for projects with just a few thousand lines of code you might find it helpful to use multiple files. The Vehicle Telemetry Platform project in Chapter 15 uses this approach to encapsulate different sections of functionality into different files, and goes one step further by using compile-time flags to skip over parts of the code that don’t need to be built if that feature is not required by the user. We’ll start by looking at the WeatherStationReceiver.h file. 250 CHAPTER 13  WEATHER STATION RECEIVER The file starts by checking for an “include guard” variable to make sure it hasn’t already been invoked, then sets the variable. This is the same technique as explained in more detail in Writing An Arduino Library in Chapter 16. #ifndef WSR_H_ #define WSR_H_ It then defines some human-readable tokens for use in the main program using a compiler directive called a “define.”Lines starting with #define are not technically part of the sketch itself, but are commands to the compiler that tell it to process the source files in a certain way. A #define sets up a convenient token that can be used elsewhere in the sketch as an alias for an actual value. The easiest way to understand how #define works is to think of it as a predefined rule for a global find/replace through the code, and that rule is run for you automatically just before the sketch is compiled. By defining a token and then using it in your code, it’s as if you wrote out the value directly. The first couple of #define entries are a trivial example, setting up aliases for variable types “unsigned int” and “signed int” so they can be referred to in the sketch as simply “uint” and “sint.” This is purely a personal preference by the programmer and not necessary for the sketch to function: variable declarations in the program could have used the original, longer version of the variable type instead, but doing this makes the sketch a little easier to read if you’re accustomed to the uint/sint convention. typedef unsigned int uint; typedef signed int sint; Next, it defines three constants starting with ASCIINUMBASE, which as its name suggests is a token representing the ASCII number base (i.e., the position of the number 0 in the ASCII code table) and is given a hex value of 0x30. Note that a define is not at all the same thing as a variable because it can’t be changed after the program has been compiled. In this case, the ASCIINUMBASE token is removed from anywhere it appears in the program code and replaced with the literal value 0x30, so doing things this way results in exactly the same program as if ASCIINUMBASE had never been defined and everywhere it occurred we had simply typed 0x30 instead. The carriage return and linefeed characters are also defined in the same way. #define ASCIINUMBASE 0x30 #define CHAR_CR 0x0D #define CHAR_LF 0x0A So far it probably sounds like a waste of time setting up all these #define entries. Often the token is longer than the value it represents, so rather than saving typing in the sketch it actually makes it longer. So why bother? The two major reasons are consistency and readability of the main program code. By defining ASCIINUMBASE once like this, and then using that token throughout the program, it’s easy to change its value in one place if necessary. For example, if you discovered that ASCIINUMBASE should really have been 0x31 instead of 0x30, how would you change all the references to it in your code? You can’t just do a global find/replace in your text editor, because you don’t know that every single instance of 0x30 in the code is actually there because that part of the code was referring to the ASCII number base. Perhaps 0x30 is also used to represent something totally different in another part of the code, and blindly replacing every instance of 0x30 with 0x31 to fix an incorrect ASCII number base value would break a totally unrelated part of the code where the value 0x30 was used for a different reason. By referring to values like this using a human-readable name such as ASCIINUMBASE you can look through the code and understand the reason that value was used in that particular context, and you don’t need to remember what the specific value is. Looking at a calculation involving the hard-coded value 0x30 doesn’t help you understand what that number represents, but a calculation involving ASCIINUMBASE is much more self-explanatory. The file then goes on to define many more entries that you’ll see used elsewhere in the program. #define WSR_TIMER_PERIOD_US 4 #define WSR_PERIOD_FILTER_MIN ( 300/WSR_TIMER_PERIOD_US) #define WSR_PERIOD_FILTER_MAX (1800/WSR_TIMER_PERIOD_US) 251 CHAPTER 13  WEATHER STATION RECEIVER #define WSR_SHORT_PERIOD_MIN WSR_PERIOD_FILTER_MIN #define WSR_SHORT_PERIOD_MAX ( 600/WSR_TIMER_PERIOD_US) #define WSR_LONG_PERIOD_MIN (1200/WSR_TIMER_PERIOD_US) #define WSR_LONG_PERIOD_MAX WSR_PERIOD_FILTER_MAX #define WSR_STATE_IDLE 0 #define WSR_STATE_LOADING_BITSTREAM 1 #define WSR_BIT_NONE 0 #define WSR_BIT_ZERO 1 #define WSR_BIT_ONE 2 #define WSR_PACKETARRAYSIZE 8 #define WSR_TIMESTAMP_BIT_OFFSET (4*8) #define WSR_RFPACKETBITSIZE 52 #define WSR_RESET() { bICP_WSR_State = WSR_STATE_IDLE; bICP_WSR_PacketInputBitPointer = WSR_TIMESTAMP_BIT_OFFSET; } You’ll notice that defines can reference each other. For example, the WSR_TIMER_PERIOD_US entry is substituted into several other entries immediately following it. Next comes the macro section. A macro is substituted into the main code just like the #define values, but rather than being a simple value it is more like a mini function that performs an operation. Once again the end result is exactly the same as if you had typed these out in full in the code rather than using their more convenient label. For example, the main program code could have included ((PORTD & (1<<PORTD6)) != 0) but reading and understanding what that does takes longer than if the code simply contains a much more self-explanatory entry, such as the following: GREEN_TESTLED_IS_OFF() The macros defined are as follows: #define INPUT_CAPTURE_IS_RISING_EDGE() ((TCCR1B & _BV(ICES1)) != 0) #define INPUT_CAPTURE_IS_FALLING_EDGE() ((TCCR1B & _BV(ICES1)) == 0) #define SET_INPUT_CAPTURE_RISING_EDGE() (TCCR1B |= _BV(ICES1)) #define SET_INPUT_CAPTURE_FALLING_EDGE() (TCCR1B &= ~_BV(ICES1)) #define GREEN_TESTLED_IS_ON() ((PORTD & (1<<PORTD6)) == 0) #define GREEN_TESTLED_IS_OFF() ((PORTD & (1<<PORTD6)) != 0) #define GREEN_TESTLED_ON() ((PORTD &= ~(1<<PORTD6))) #define GREEN_TESTLED_OFF() ((PORTD |= (1<<PORTD6))) #define GREEN_TESTLED_TOGGLE() if(GREEN_TESTLED_IS_ON()){GREEN_TESTLED_OFF();}else{GREEN_TESTLED_ON();} #define RED_TESTLED_IS_ON() ((PORTD & (1<<PORTD7)) == 0) #define RED_TESTLED_IS_OFF() ((PORTD & (1<<PORTD7)) != 0) #define RED_TESTLED_ON() ((PORTD &= ~(1<<PORTD7))) #define RED_TESTLED_OFF() ((PORTD |= (1<<PORTD7))) #define RED_TESTLED_TOGGLE() if(RED_TESTLED_IS_ON()){RED_TESTLED_OFF();}else{RED_TESTLED_ON();} Finally, the WeatherStationReceiver.h file ends by closing the “if” check on the include guard. #endif Next we get to the main program file itself, WeatherStationReceiver.pde. Even with all the previous defines simplifying things, this is still a big chunk of code so we’ll go through it in little steps. Before getting into the details, though, it’s worth looking at the overall structure of the program and understanding what it is trying to do. 252 CHAPTER 13  WEATHER STATION RECEIVER To save battery power the weather station transmitter only powers up when it needs to send an update. This can cause problems for the inexpensive 433MHz receiver used in this project because it isn’t “squelched,” which means it just listens continuously to whatever is floating around at that frequency and don’t disable its output if the input-level signal received is below a certain power level. If you’ve heard a CB radio receiver with the squelch control turned right down or an AM/FM radio mistuned and playing nothing but static, that’s exactly what the receiver blasts at the Arduino whenever the transmitter isn’t sending a message. The trick that this program performs is listening to that continuous blast of random noise and figuring out the difference between meaningful bit values and meaningless static. The receiver’s output is fed to a special pin designated as an Input Capture Pin (ICP) that the CPU can process in a different way to other I/O pins. The ICP provides very accurate timing and edge detection so that the CPU can examine the shape of the waveform being presented to it, and analyze it for specific characteristics that represent meaningful data. In the case of La Crosse weather stations the data is sent as a pulse-width and transition-encoded bitstream, so it’s not enough to simply treat it as a simple stream of binary data like a serial port. First, the program includes the header file we examined earlier so that all the #define entries will be available. #include "WeatherStationReceiver.h" Next, it defines some variables that will be used later in the program. You’ll notice that, in this case, the variables have been named using a convention that prepends each one with the type of the variable so that later in the program it will be easy to see what type of data each one can contain. For example, all the “byte” type variable names start with “b,” the “signed int” types with “si,” the “unsigned int” types with “ui,” and so on. Once again this is just a matter of programmer preference and doesn’t change the meaning of any of the variables. uint uiICP_PreviousCapturedPeriod; byte bICP_CapturedPeriodWasHigh; byte bICP_PreviousCapturedPeriodWasHigh; unsigned long ulICP_Timestamp_262_144mS; byte bICP_WSR_State; byte bICP_WSR_PacketData[WSR_PACKETARRAYSIZE][4+8]; byte bICP_WSR_PacketInputPointer; byte bICP_WSR_PacketOutputPointer; byte bICP_WSR_PacketInputBitPointer; uint uiICP_WSR_ReceivedPacketCount; unsigned long ulWSR_LastTimestamp_262_144mS; byte bWSR_StationTransmitterID; sint siWSR_CurrentTemperature; byte bWSR_CurrentHumidity; byte bWSR_CurrentWindDirection; uint uiWSR_CurrentWindSpeed_m_per_sec; uint uiWSR_RainfallCount; unsigned long ulWSR_Rainfall_mm_x10; The last variable item defined next is a multidimensional array, or an array of arrays. It has 16 elements, each of which holds a subarray containing four elements. It’s also prepended with the “const” (constant) keyword to tell the compiler that this variable can never be modified within the program, and can only be referenced. In Arduino sketches declaring const causes the value to be placed into flash memory along with the program itself, a great way to save RAM space and store large blocks of text, lookup tables, and other large unchanging data. In this case, the 16-element array of four character text strings are used to hold the wind direction labels reported out the serial port in readable text. 253 CHAPTER 13  WEATHER STATION RECEIVER Note that there is always a hidden NULL (0x00) byte at the end of a string so that any print or string handling function knows that a string ends when it sees the NULL, which is why the three-character strings such as “NNE” are declared as four bytes each. const char strWindDirection[16][4] = { "N ", "NNE", "NE ", "ENE", "E ", "ESE", "SE ", "SSE", "S ", "SSW", "SW ", "WSW", "W ", "WNW", "NW ", "NNW" }; To help with figuring out the communications protocol and to see more detail about what the program is doing, you can define DEBUG at the start of the program to change how it behaves. With this line commented out as shown next the program will run as normal, but if you remove the double slash and recompile the program it will run in debug mode and output additional information. //#define DEBUG The setup routine appears very simple because it hands off most of the work to a pair of initialization functions that we’ll examine in a moment. It also opens a serial connection back to a host computer at 38400bps and says hello. void setup(void) { Serial.begin( 38400 ); Serial.println( "Weather Station Receiver has powered up" ); Init_Ports(); Init_RF_Interpreters(); interrupts(); } The main program loop looks even simpler because it just calls the same function over and over again as fast as possible. Most of the bitstream processing is invoked by interrupts so it doesn’t need to be explicitly done here. void loop(void) { Packet_Converter_WS2355(); } The next function sets DDRB to a specific value. DDRB is the Data Direction Register for port B, so it sets the input/output mode of digital pins 8 through 13. Explicitly setting DDRB to a specific value is simply a shorthand and very efficient equivalent to making a whole series of calls similar to “pinMode(8, INPUT)”. Setting the registers directly like this is very common in microcontroller projects, but it’s not generally considered to be “the Arduino way” because it’s more opaque and harder to debug than a series of individual calls. Looking at the following code, you can’t tell which pins are affected unless you happen to know that port B is connected to Arduino digital I/O pins 8 through 13, and even if you did know that it’s not obvious which way around the byte is applied. Is pin 8 the first bit and being set to an output, or is it the last bit and being set to an input? You can’t tell just by looking at it. In this example, pin 8 is represented by the least significant bit at the end of the byte and pin 13 is represented by the most significant bit at the start of the byte. This is much more confusing than a simple call to pinMode, but this technique can be very useful in some situations so it’s worth keeping in mind for future reference. void Init_Ports() { DDRB = 0x2F; // B00101111 } 254 CHAPTER 13  WEATHER STATION RECEIVER The main loop calls the packet converter function repeatedly. It doesn’t take any arguments because it operates on global variables that are set elsewhere in the program. Each time through it checks whether there is a fresh message packet waiting to be processed by comparing the packet input pointer and the packet output pointer. void Packet_Converter_WS2355(void) { byte b; byte c; sint si; if( bICP_WSR_PacketInputPointer != bICP_WSR_PacketOutputPointer ) { While in debug mode, the program outputs a spacer line if it has been more than about two seconds (actually, 8 × 144 milliseconds), so that individual packets are visually separated in the serial monitor. #ifdef DEBUG if( (ulICP_Timestamp_262_144mS - ulWSR_LastTimestamp_262_144mS) > 8 ) { Serial.println(); } #endif While trying to figure out the communications protocol it can be very handy to output the raw message packet so you can see what is being sent. We only want that to happen if the DEBUG flag has been set, though, so first we check if it has been defined. Then the binary form of the message is sent to the host via the serial port by walking through it one bit at a time. #ifdef DEBUG Serial.print("BINARY="); for( b = WSR_TIMESTAMP_BIT_OFFSET; b < (WSR_RFPACKETBITSIZE+WSR_TIMESTAMP_BIT_OFFSET); b++ ) { if( (bICP_WSR_PacketData[bICP_WSR_PacketOutputPointer][b >> 3] & (0x80 >> (b&0x07))) != 0 ) { Serial.print( '1', BYTE ); } else { Serial.print( '0', BYTE ); } if( b == 31 ) Serial.print( ' ', BYTE ); } Serial.println(); The value is also printed out in hexadecimal form in the same way, working through it one nibble (half-byte) at a time. On each pass through the loop it prints out the top nibble of the byte followed by the bottom nibble, but on the last pass through it doesn’t print the bottom nibble because it’s not part of the 52 incoming bits that form the message. Serial.print("HEX="); for( b = 0; b < ((WSR_RFPACKETBITSIZE+WSR_TIMESTAMP_BIT_OFFSET)/4); b += 2 ) { Printing the top nibble is as follows: c = bICP_WSR_PacketData[bICP_WSR_PacketOutputPointer][b >> 1]; Serial.print( (c & 0xF0) >> 4, HEX ); 255 CHAPTER 13  WEATHER STATION RECEIVER Then printing the bottom nibble, but only before the last byte, is as shown here: if( b < (((WSR_RFPACKETBITSIZE+WSR_TIMESTAMP_BIT_OFFSET)/4)-1) ) Serial.print( (c & 0x0F), HEX ); After the sixth byte a space is printed to separate out the timestamp from the rest of the message. if( b == 6 ) Serial.print( ' ', BYTE ); } Serial.println(); #endif A little later in the program we define a function to calculate the checksum of the message that has been received and return either true or false depending on whether it passes or fails. Now that the message has been loaded into a global variable, the checksum calculation is called and the rest of the function is only executed if it returns true. if( PacketAndChecksum_OK_WS2355 ) { Each weather station transmits a pseudo-unique station identifier that is generated each time it powers up. The station identifier is printed out in case you need to differentiate between multiple weather stations in the same area. b = (bICP_WSR_PacketData[bICP_WSR_PacketOutputPointer][5] << 4); b += (bICP_WSR_PacketData[bICP_WSR_PacketOutputPointer][6] >> 4); bWSR_StationTransmitterID = b; Serial.print( "STATIONID=" ); Serial.println( bWSR_StationTransmitterID, DEC ) Rather than sending one data packet containing readings from all the sensors at once, the La Crosse transmitter acts as a gateway and passes on values from the various sensors individually as they become available. Each message packet therefore contains the data for just one sensor, and after working through the first part of the header containing the timestamp and the station ID the next part of the header contains a sensor ID in bits 4 and 5. Obviously with only two bits to store the sensor ID there are only four possible IDs: b00 (decimal 0), b01 (decimal 1), b10 (decimal 2), and b11 (decimal 3). Those four IDs correspond to temperature, humidity, rainfall, and wind sensors, respectively. To extract the sensor ID we grab the appropriate byte from the message packet, then apply a shift- right operator and apply a logical AND to mask out the rest of the byte that we don’t care about. b = bICP_WSR_PacketData[bICP_WSR_PacketOutputPointer][5]; b = (b >> 4) & 0x03; Then it’s a simple matter of checking the value of the sensor ID and processing the rest of the message to suit the requirements of that particular sensor. Sensor ID 0 is the temperature sensor which puts the first temperature digit in the lower nibble of byte 7. We extract that and multiply it by 100 because it’s in the hundreds column, then put the value into the si variable. We then move on to processing the additional temperature digits in the upper and lower nibbles of byte 8, multiplying the tens digit by 10 and simply adding the ones digit as is. switch( b ) { case 0: { si = ((bICP_WSR_PacketData[bICP_WSR_PacketOutputPointer][7] & 0x0F) * 100); si += ((bICP_WSR_PacketData[bICP_WSR_PacketOutputPointer][8] >> 4) * 10); si += (bICP_WSR_PacketData[bICP_WSR_PacketOutputPointer][8] & 0x0F); At this point we’re very close to having the value we need, but because the value that is sent through is an unsigned number it can’t represent negative values and it also doesn’t have a decimal point. Not much good if the temperature drops below 0. The number sent through is, therefore, multiplied by 10 to 256 CHAPTER 13  WEATHER STATION RECEIVER remove the decimal place and then offset by 300 above the actual value, so to determine the real value we subtract 300 from whatever value the sensor reported and then divide by 10. siWSR_CurrentTemperature = (si - 300); The value is then printed to the host via the serial port, but even this requires a little trick because after we’ve divided the number by 10 it will become a decimal value and can’t be passed directly to Serial.print. Therefore we print the divided value which will print just the part before the decimal point, then manually send a period, then send the modulus of the temperature to get just the decimal part. Serial.print("TEMPERATURE="); Serial.print( (siWSR_CurrentTemperature/10), DEC ); Serial.print( '.', BYTE ); if( siWSR_CurrentTemperature < 0 ) { Serial.println( ((0-siWSR_CurrentTemperature)%10), DEC ); } else { Serial.println( (siWSR_CurrentTemperature%10), DEC ); } break; } Sensor ID 1 is the humidity sensor, which is treated in almost the same way as temperature. It’s actually a little simpler, though, because we don’t need the 300 offset (humidity can never be negative!), and it’s always a whole number so there’s no need to mess around with printing a decimal value to the host. case 1: { c = ((bICP_WSR_PacketData[bICP_WSR_PacketOutputPointer][7] & 0x0F) * 10); c += (bICP_WSR_PacketData[bICP_WSR_PacketOutputPointer][8] >> 4); bWSR_CurrentHumidity = c; // Print to serial port Serial.print("HUMIDITY="); Serial.println( bWSR_CurrentHumidity, DEC ); break; } Sensor ID 2 is the rainfall sensor. After dealing with the last two sensors the routine should be familiar by now: grab the appropriate bytes out of the packet, add them together, and apply a transformation to convert it to the correct units which in this case is millimeters of rain. case 2: { si = (sint)(bICP_WSR_PacketData[bICP_WSR_PacketOutputPointer][7] & 0x0F) << 8; si += bICP_WSR_PacketData[bICP_WSR_PacketOutputPointer][8]; uiWSR_RainfallCount = (uint)si; ulWSR_Rainfall_mm_x10 = (((unsigned long)uiWSR_RainfallCount * 518) / 100); Serial.print("RAINFALL="); Serial.print( (ulWSR_Rainfall_mm_x10/10), DEC ); Serial.print( '.', BYTE ); Serial.println( (ulWSR_Rainfall_mm_x10%10), DEC ); break; } The final sensor ID is 3 and, though it’s similar to the others, it does have a slight twist to it: this particular packet contains two values within the same message. Wind speed and direction are bundled 257 CHAPTER 13  WEATHER STATION RECEIVER together, with the speed taking up the first byte-and-a-half of data and the final nibble providing the direction. Therefore we start by doing a logical AND to mask off half the second byte and process it to get the wind direction, then perform similar tricks to extract the wind speed. We don’t just print out the wind direction directly, though, because it’s a number that won’t mean much. Instead, we use that number to reference a specific element in the wind direction array we defined way back at the start of the program and extract the string found there to display a human- readable value such as “NNW.” case 3: { bWSR_CurrentWindDirection = (bICP_WSR_PacketData[bICP_WSR_PacketOutputPointer][8] & 0x0F); //wind speed, decimal value is metres per second * 10 (1 fixed deciml place) si = (sint)(bICP_WSR_PacketData[bICP_WSR_PacketOutputPointer][7] & 0x10) << 4; si += ((bICP_WSR_PacketData[bICP_WSR_PacketOutputPointer][7] & 0x0F) << 4); si += (bICP_WSR_PacketData[bICP_WSR_PacketOutputPointer][8] >> 4); uiWSR_CurrentWindSpeed_m_per_sec = (uint)si; Serial.print("WINDDIRECTION="); Serial.println( strWindDirection[bWSR_CurrentWindDirection] ); Serial.print("WINDSPEED="); Serial.print( (uiWSR_CurrentWindSpeed_m_per_sec/10), DEC ); Serial.print( '.', BYTE ); Serial.println( (uiWSR_CurrentWindSpeed_m_per_sec%10), DEC ); break; } The code should never get to the default section of the case statement because sensor IDs should only ever be from 0 to 3. Then we wrap up the alternative to the if condition on the checksum test so that if the checksum failed we print a message saying so. default: { break; } } } else { Serial.print( " Bad checksum or packet header" ); } The timestamp variable needs to be updated so that next time through it can be checked to see if a blank line needs to be inserted between packets. ulWSR_LastTimestamp_262_144mS = ulICP_Timestamp_262_144mS; Now that we’re done with this message packet the output pointer can be moved along. bICP_WSR_PacketOutputPointer = ((bICP_WSR_PacketOutputPointer+1)&(WSR_PACKETARRAYSIZE- 1)); } } 258 . just download it from the project page on the Practical Arduino web site at www.practicalarduino.com /projects/ weather-station-receiver. Unlike many Arduino programs it’s not all contained within. your Arduino and connect it to your computer via USB (see Figure 13-12). 249 CHAPTER 13  WEATHER STATION RECEIVER Figure 13-12. Arduino, receiver, and antenna assembled and ready for use. Receiver Program The program for this project is one of the more complex ones in the book and uses a number of things you might not have seen in Arduino sketches before. It’s fairly long, so you

Ngày đăng: 03/07/2014, 20:20

TỪ KHÓA LIÊN QUAN