Wednesday, July 19, 2017

FSX with Live Camera Windowed Views (Part 1)



Tutorial for OpusFSI Live Camera Window Views on Client Displays for FSX — 

Purpose: Microsoft Flight Simulator (FSX) provides a means for displaying multiple graphics views on a PC with one or more extended displays. Using extended displays spanning 2 or more monitors provides a mechanism for viewing outside the aircraft, both left and right, as an aid for making the simulation more realistic. Additional views are especially useful for landing approaches, both VFR and IFR. Extended displays across two or more monitors is set up in Windows:Control Panel:...Display:Screen Resolution.

FSX on one PC with multiple views on several displays can easily overwhelm the system causing software crashes during a flight simulation. One approach to fixing this problem is to run FSX on two or more PCs which spreads the load and reduces the occurence of crashes.

Having FSX run on more than one PC requires some intermediate software to coordinate the simulations and displays between the PCs. One such Product is WidevieW, and a second is OpusFSI.

Wideview was purchased and tested but had issues with aircraft bouncing around on the ground and the software was abandoned without much luck.

OpusFSI was purchased and installed and provides good integration of views across two PCs. However, I was not able to find a detailed example of how to create multiple views in a fairly seamless manner. Opus Software provides a vast amount of documentation, but the documentation lacks examples of how to implement specific uses of the software. This tutorial describes one way of using the OpusFSI software to create side views from the perspective of the pilot of the simulator. The software vendor or other users may have other approaches to achieve the same end result.


Hardware/Software Configuration:

The hardware used for this tutorial include two PCs: 1) Intel Core-2 with Win7 32-bit Laptop with 2 displays (laptop display and Mini Display Port to VGA display); 2) Intel i7 with Win8.1 64-bit desktop with 2 displays (HDMI and Display Port, both 1080P). 

The Win7 laptop PC software includes: 1) FSX with Acceleration, 2) Mobiflight for home-built controls, 3) FSUIPC; and, 4) OpusFSI_V5 Server for FSX.

The Win8.1 desktop PC software includes:  1) FSX with Acceleration, 2) FSUIPC; and, 3) OpusFSI_V5 Client for FSX.

The OpusFSI documentation was followed for installing the Server and Client software, for setting the file sharing and permissions, and for establishing the communication via ethernet on home network between the Server and Client. The Live View Test, run on the Client, then the Server, will demonstrate whether the Server is communicating with the Client over the local network. Those tests must work before further installation tasks can be accomplished. These tests do not test if file sharing and permissions are set correctly, but will establish that the Server actually can control the Client. The Opus documentation discusses in detail the requirements for setting sharing and permissions.

The Server window "Configure" button is used to configure the number of Client machines, to enable Live Camera, and to provide the path to the FSX install directory.

The Server window "Cameras" button provides a window for defining and managing views. For this tutorial, 2D Windowed views will be created for the Cessna 172 aircraft or for all aircraft. Using the views for all aircraft is a bit easier for first use.


Software Start-up:

Start both the Server and Client PCs.
Run the FSX software with FSUIPC on both machines.
Run the FSI Server on the Server PC using a shortcut target "C:\OpusFSI_v5\FSISERVER.EXE FSX" as Administrator.
Run the FSI Client on the Client PC using a shortcut target "C:\OpusFSI_v5\FSICLIENT.EXE FSX" as Administrator.

The FSIServer and FSIClient will add an Add-On to the FSX Add-Ons menu along with the FSUIPC Add-On.

Have a copy of the same FSX flight on both machines. Load that same FSX flight on the Server FSX and the Client FSX. Both PCs should display the same view on startup if FSI Live Views have not been created. On the Client machine, use menu FSX:Options:Settings:Display and set the 2D display as the default and, if desired, set the transparency to 100%. This will hide the dashboard panel from showing, however, it will still exist and can cause some confusion when moving views that overlap with the transparent panel display. Save the changes to the Client aircraft flight.


Live Views:

Client Live Views are created using the FSI Server window, "Cameras" button, and Camera Management window. 
1) On the Camera Management window, first click the "Select Computer System" button and choose the Client PC by name. There should be both the Server PC and Client PC names displayed on the list of choices. The field to the right of the button will display the chosen Client PC name. 
2) Choose "All Aircraft Types", and "Create" to create a new view. 
3) Click "2D Cockpit", "OK", and the "Camera Editing" window will display. 
4) Rename the "Camera" named "Camera View x" to something meaningful to you. 
5) Choose "Windowed View", "2D Cockpit", set X to "-3", set Y to "1", set Pitch to "6.4", and set Yaw to "-40". You should see a full window view on the Client screen with a view 40 degrees to the left of front from the cockpit. The X=-3 moves the view to the left outside the cockpit and the pitch and yaw rotate the view to the desired angle. Push "OK" and "OK" again to return to the FSI Server window.

What will seem odd at this point is that the Client display will not show the new view that was just created. To get the view to display requires that a different aircraft (Not the same type, just choose the Piper Cub or something else) and the new view will display. Now go back and choose your desired aircraft and the rotated view should display. The Instrument Panel or 3D Virtual Cockpit may also show, you just have to remove those from the display.


Adjust the Views on the Client Displays:

For one or more displays, set your FSX window to "not full screen" and expand it to span across your multiple displays, as big as you want your FSX window to be. 

According to the OpusFSI documentation, you can have a view that is either "docked" or "undocked". A docked view will reside inside the FSX main window, while an undocked view can be moved outside the FSX main window. However, FSX will not load a view as "undocked" and any view that you want as undocked will require manual intervention each time the software loads. To fix that problem, the FSX window can be made as large as the multiple displays will allow. Then the "docked" views can be loaded, sized, and moved to the desired locations automatically as the flight loads.

1) Resize the Client FSX window as large as you desire.
2) Move and resize your Windowed View(s) to where you want it on your Client display inside of the FSX window.
3) Save your aircraft using the FSX:Flights:Save... using your current flight name. At this point the Client flight is a bit different than the Server flight of the same name, but that is OK. Saving the aircraft will save the large size of the FSX window.
4) On the Client FSX menu choose "Add-Ons:OpusFSI:Save Windowed Views" to save the view size and location within the FSX window. If a Client view is then moved, clicking "Add-Ons:OpusFSI:Restore Windowed Views" will move the views back to the locations and sizes when they were last saved.

This process can be used to create multiple views that will load automatically on the Client when FSX/FSUIPC/FSI are started on the Server and Client.

Issues:

As mentioned earlier, "undocked" views must be manually undocked. This can be mostly overcome by expanding the FSX window and just displaying the Windowed Views as "docked" views within the FSX window. 

According to Opus Support, if an "undocked" view is desired, a "docked" view can be "undocked" (right mouse click) and the "Add-Ons:OpusFSI:Restore Windowed Views" will move the views back to the locations and sizes when they were last saved, including the now "undocked" views which may be moved to locations outside the FSX window. I was not able to get that functionality to work, so development focused on using docked views within the FSX window spanned across multiple displays.

A second issue arises if the Client FSX goes down by a crash or through manually stopping the FSX software. If I am in a lengthy simulation and the Client PC FSX crashes, I would prefer to restart the Client and continue with the flight. What happens when the FSX Client stops on my system is that the C172 aircraft.cfg file gets over-written when the FSX Client is restarted. At that point, the aircraft.cfg no longer contains the Windowed View definitions, so they can not load and display on the Client.

I have found a couple of fixes for this problem that may be a bug in my system that I do not know how to fix at this time.

1) After restarting the Client FSX but before selecting the flight/aircraft, replace the aircraft.cfg file without the view configurations with a copy of the file that contains the view configurations. Use Notepad++ to edit the .cfg files and you will find where the missing view configurations should be.

The default aircraft.cfg file used by FSI will have the following lines at about line number 476:

=============== Example aircraft.cfg without Windowed Views ===============
[deice_system]
structural_deice_type=0     //0 = None, 1 = Heated Leading Edge, 2 = Bleed Air Boots, 3 = Eng Pump Boots

[CameraDefinition.899]
Title = "OpusFSI Aircraft View"
Guid = {01021987-E220-6507-1024-462840738899}
Description = OpusFSI Aircraft View
Origin = Center
SnapPbhAdjust = Swivel
SnapPbhReturn = FALSE
PanPbhAdjust = Swivel
PanPbhReturn = FALSE
ShowAxis = FALSE
ShowWeather = TRUE
Category = Aircraft
MomentumEffect = FALSE
ShowLensFlare = FALSE
CycleHidden = No
PitchPanRate = 30
HeadingPanRate = 75
PanAcceleratorTime = 0
SmoothZoomTime = 2.0
AllowZoom = TRUE
XyzAdjust = TRUE
AllowPbhAdjust = TRUE
InitialZoom = 0.8
InitialXyz = 0,6,-30
InitialPbh = 6,0,0
Track = None

[CameraDefinition.0]
Title = "Right Wing"
Guid = {C690EAFD-223A-42d0-99E0-681ADF93BB59}
Description = View from the right wing tip looking at the cockpit
Origin = Center
SnapPbhAdjust = Swivel
...

===============================================================


=============== Example aircraft.cfg with Windowed Views =============
[deice_system]
structural_deice_type=0     //0 = None, 1 = Heated Leading Edge, 2 = Bleed Air Boots, 3 = Eng Pump Boots

[CameraDefinition.899]
Title = "OpusFSI Aircraft View"
Guid = {01021987-E220-6507-1024-462840738899}
Description = OpusFSI Aircraft View
Origin = Center
SnapPbhAdjust = Swivel
SnapPbhReturn = FALSE
PanPbhAdjust = Swivel
PanPbhReturn = FALSE
ShowAxis = FALSE
ShowWeather = TRUE
Category = Aircraft
MomentumEffect = FALSE
ShowLensFlare = FALSE
CycleHidden = No
PitchPanRate = 30
HeadingPanRate = 75
PanAcceleratorTime = 0
SmoothZoomTime = 2.0
AllowZoom = TRUE
XyzAdjust = TRUE
AllowPbhAdjust = TRUE
InitialZoom = 0.8
InitialXyz = 0,6,-30
InitialPbh = 6,0,0
Track = None

[CameraDefinition.990]
Title = "OpusFSI Windowed View 1"
Guid = {01021987-E220-6507-1024-462840738990}
Description = OpusFSI Customized Scenic View
Origin = Cockpit
SnapPbhAdjust = Swivel
SnapPbhReturn = FALSE
PanPbhAdjust = Swivel
PanPbhReturn = FALSE
ShowAxis = FALSE
ShowWeather = TRUE
Category = Custom
MomentumEffect = FALSE
CycleHidden = No
PitchPanRate = 30
HeadingPanRate = 75
PanAcceleratorTime = 0
SmoothZoomTime = 2.0
AllowZoom = TRUE
XyzAdjust = TRUE
AllowPbhAdjust = TRUE
InitialZoom = 1.0
InitialXyz = -3,1,0
InitialPbh = 6.4,0,-40.5595
Track = None

[CameraDefinition.0]
Title = "Right Wing"
Guid = {C690EAFD-223A-42d0-99E0-681ADF93BB59}
Description = View from the right wing tip looking at the cockpit
Origin = Center
SnapPbhAdjust = Swivel
SnapPbhReturn = FALSE
...
=============================================================

On my Client PC, anytime the Client FSX and FSI software is shutdown, all of the aircraft.cfg files revert to the default copies. To fix this problem, make backup copies of the files that contain the additional view definitions.

In addition, the view definitions should also exist in the Client C:\OpusFSI_V5\OpusFSI_Server_CamDefs.txt file. The contents of this file can be pasted into the default aircraft.cfg to add the view definitions manually. See the above example for the location of these definitions in the .cfg file.

To prepare files for this fix on the Client, when the Client views are properly displayed, navigate to "C:\Program Files\Microsoft Games\Microsoft Flight Simulator X\SimObjects\Airplanes\(your aircraft)\aircraft.cfg", copy the file, paste the file as a copy, and rename it so that you have a copy to use when required (after a Client FSX shutdown or crash). If this copy is recopied over the file without the view definitions, and then the FSX flight is chosen on the Client, the flight and aircraft will load with the saved views. You may need to "Open" or "Restore" the Client Windowed views, but the views should display properly and the current flight on the Server can resume with little impact.

You can force the Client to retain the aircraft.cfg file with the added Live View definitions by changing the Properties:Read Only and maybe also turning off Everyone permissions on the file or folder. This fix just stops the modified files from being over-written by the Server so the views are always defined. Any desired changes to the views would require an update of the aircraft.cfg files with the revised view definitions. (Just one more thing to have to remember)

A second fix to the missing aircraft.cfg view definitions, is to use FSI to write the proper files from the Server back to the Client. To do this, have the Client FSX/FSUIPC/FSI running,  but do not load the flight, yet.

On the Server, use the FSI Server window, click "Cameras" button, choose any one of the 2D Windowed views on the Client, "Edit", to display the edit window, then push "OK", "OK" twice to exit the Edit window back to the FSI Server window. The Server will update the necessary files on the Client. If the flight has not yet been loaded on the client, do so now. If a flight was already active, select a different type of aircraft to force the loading of the modified files, then select your aircraft, and use "Open" or "Restore" if nessary to put the views in the desired locations.

If you find a better method or fix a bug in what has been described above, please share your knowledge.

Thanks



Friday, May 26, 2017

Arduino and FSX - Update for Rudder Pedals and Brakes

Update for Rudder Pedals and Brakes — 
'Arduino and FSX' blogs, Part 3 and Part 4 discussed using an Arduino UNO as a joystick with buttons and axes. In Part 4, rudder and brake pedals were built using plans from Bruce May (How To Build Rudder Pedals, 2006). My experience with those pedals as constructed was less than desired, so a rebuild was in order. The problems that required fixing were: 1) sticky movement of the pedals; and, 2) the pedals did not self-center the rudder as foot pressure was released. The toe brakes worked very well and did not require changes to the original design. The Arduino joystick wiring and programming did not require any changes, other than recalibration of the X, Y, and Z axes in FSUIPC since the rebuild changed some of the sensor positions.



Photo 1: Original Pedal Design: a metal bracket was added to limit toe braking when moving rudder.   
 
Photo and Figure 2: Revised Pedal Design: Toe brake hinged above rudder pedal.
The revised plan was to place each pedal and toe brake on a drawer-slide trolley to provide metal tracks for the trolley wheels to move without binding or dragging. The second change was to fix the lower part of the pedal in place so pushing the rudder pedal did not also cause braking. The third change was to move the toe brakes above the rudder pedals. And lastly, to modify the return springs so the pedals would have good tension while providing improved self-centering.


Photo 3: Pedal Trolley rolls between side rails and is centered by tensioner spring fastened to center of trolley base and each end is fastened to base plate. Trolley must be mounted to provide space for tensioner spring.




Photos 4, 5, 6: Trolley Side Rails are cut to 20" length. Trolley Roller Slides are cut to 12" length. A second 3/4" roller is mounted in cut-out on the 12" Trolley Roller assembly. The 4" x 12" Trolley plywood base is mounted between the Left and Right Trolley Roller assemblies. The Trolley Side Rails are snugged to the Trolley Roller Slides to give a smooth fit (See Photo 3).


The base plate of the pedal platform is 20"x20" plywood. The revised pedal trolley platforms are 4"x12" plywood, mounted between two modified drawer slides (think of a kitchen cabinet drawer with just the bottom piece of the drawer that slides front to back). The 4" wide by 12" long trolley can move forward 4" and backward 4". Centered under the trolley is a 10.25" spring that is stretched and secured 1" from the front and rear of the base plate. The center of the spring is attached to the bottom of the trolley with a figure-8 of wire and a small screw. The trolley side rails must be mounted sufficiently high to allow space for the tensioning spring under the trolley. The trolley can then move fore and aft with the desired pressure and return to the center position (with some dead space error) as foot pressure is released.
Attaching the pedal spring to the bottom of the pedal trolley fixed several problems. In the previous design, the return spring had been connected between the angle bar that rotated as the pedals were moved. The spring pulled on the angle bar tending to twist the bar against its center pivot pin causing friction, gouging, and wear of the aluminum bar. The result was tension on the bar that led to sticky and erratic pedal movement. In the new design, the spring tension is inline with the pedal so that foot pressure is easily transferred between the foot, the pedal, the spring, and the base, and the spring can stretch fore and aft without binding.
The earlier design had a flat pedal that served to move both the rudder and toe brake. I found that design difficult to use while taxiing since movement of the rudder for steering usually caused one or the other differential brakes to also be applied. In the revised design, the toe brake is moved to a separate hinged piece at the top of the pedal assembly. The rudder pedal is now a 4"(W)x6.5"(L) piece of plywood mounted at a 35-degree angle with a 4"(W)x3"(L) piece of plywood toe brake mounted edge-to-edge with a 3" cabinet hinge. The toe brake rotates forward and down by pressing forward and down with the toes while rudder control and steering is provided by fore and aft movement of the pedal and trolley by the heel of the foot. I find this design works well for me.
Construction Materials -
1 - 24" x 48" x 5/8" or 3/4" plywood
4 - 20" x 1-5/16" x 3/4" slide rail support (or 20" x 1-3/8" (as needed) x 3/4")
2 - 20" x 1.5" plywood rear bumper strip
2 - 7/16" x 10.25" x 0.041" springs (Everbilt SKU 683-948 from Home Depot)
2 - 3" cabinet hinges
4 - 3/4 in. nylon sliding shower door rollers, #8x32 screws, 11/64" drill hole
2 - sponge balls
assorted #8 3/4" and 1 1/4" wood screws
Other parts as specified by Bruce May (2006)
Base plate: 20" x 20" plywood
Pedal trolleys: 4"(W) x 12"(L) plywood
Pedals: 4"(W) x 6.5"(L) plywood
Toe brakes: 4"(W) x 3.5"(L) plywood, rounded as desired
Pedal supports: 5.5" x 5.5" plywood, cut to shape, see Figure 2
Pedal support cross member: 2" x 3" plywood

Useful references -
 (May 26, 2017)

Saturday, February 4, 2017

FSX Learning Center Flight Training Download

FSX Learning Center

MS Flight Simulator 2004, 2009, and FSX contained valuable flight ground school training from Microsoft FSX Learning Center. Those file links are still on a 2009 Microsoft web site, but the links do not work.

The Learning Center Ground School files are contained in two directories of HTM and BMP files, and they display well on Google Chrome or Safari. The html files use frames, so they do not display well if the files are copied to an iPad (I could not make that work for off-line study). These files have links to the "Fly This Lesson Now" which do not work without the FSX software. The start file is /Uires/lc01.htm which can be aliased and renamed if desired.

I have put the files on Google Drive for download. I hope I am not breaking any copyright laws by doing so. I saw that people were not able to get the training in the new Steam FSX, so hopefully this might help. You might get a warning that the file is too big (80 Mb) for Google Drive to check for viruses. The file I published on Google Drive was virus free. You may want to scan it just to make sure it has not been infected. Good flying.

Lowell


Thursday, February 2, 2017

Arduino with 8 KY040 Rotary Encoders and 16 Switches

Adding 8 Rotary Encoders and 16 Switches to an Arduino Mega 2560

In the last blog, "Arduino with 8 KY040 Rotary Encoders", I made passing reference that switches could be added into the sketch. The Microsoft flight simulator software, FSX, can accept data from an Arduino when it is programmed as a HID-USB joystick with 8-axes and 32 buttons or switches. The joyReport data structure within the sketch holds and conveys the axes, switches, and encoders data through USB to the simulator software on a PC. Eight (8) rotary encoders add 16 switches to the joyReport, so there are 16 additional bits of data that can be added from other pins on the Mega 2560.

The 8 rotary encoders were added to the sketch by setting 2 8-bit ports (Port B and Port K) as pin-change-interrupts, effectively adding 16 additional hardware interrupts to the Mega 2560. The analog pins A0-A7 are used in the sketch for axis sensors (potentiometers). The 6 external hardware interrupts on the Mega 2560 are pins 21, 20, 19, 18 (INT0, INT1, INT2, INT3) and, pins 2 and 3 (INT4, INT5), which can be used for additional switches that require immediate attention of the software. Thirty-eight (38) remaining digital pins can be used for switches, but without a hardware interrupt, polling is required to cycle through the pins to determine if a pin has changed state. In FSX, buttons and switches are generally used to turn on and off lights or other airplane functions that are slow human-speed activities, so polling of switch states will not degrade simulator performance to any significant degree. Interrupts, rather than polling, were used for encoders since the timing of the A-B switching of the encoder was critical to reading the encoder accurately. Since changed switch states happens so rarely (relative to CPU speed), all the switch states can be accumulated into the joyReport data structure, and that data structure can then be transmitted when any one or more switches has changed HIGH-LOW or LOW-HIGH. Any switch that stays ON or OFF will have its value transmitted in the joyReport each time the joyReport is sent out through USB. The FSX/FSUIPC software on the PC has to deal with the repetition of switch setting values.

Sketch Design --

In the sketch, the existing Axes (pins A0-A7) will be retained. Also the 8 rotary encoders which write their 16 values into joyReport bytes [0] and [1] will be retained.
Sixteen digital pins will be added to populate joyReport bytes [2] and [3]. And for demonstration, 2 of 6 hardware interrupts will be used to capture switch values, and those values will be written to joyReport byte [4] bits [1,0]. FSX does not use the data from byte 4, but the programming will have been provided for anyone wanting an example of how to use those interrupts.

Sketch --


/*

   Sketch - Arduino_ISR_KY040_Mega2560_16_8X40Joystick_Oleg

   Lowell Bahner
   February, 2017

   This code sends joyStick data when rotary encoders change values.

   This code adds 16 digital pins for switches plus 2 hardware interrupt pins
   for switches.

   This code uses 16 pins on 2 ports on a Mega2560 for 8 KY040 rotary encoders.
   Other encoders may also work but have not been tested with this code.
   Each encoder pin is on a separate port (Port B and Port K)
   Ref: www_atmel.com/Images/Atmel-2549-8-bit-AVR-Microcontroller-ATmega640-
               1280-1281-2560-2561_datasheet.pdf

   8 Rotary Encoders are connected to the 16 Arduino pins. Each encoder CLK "A" pin
   is connected to a successive Mega Port B pin [0,1,...7] and the encoder DT "B" pin
   is connected to the Mega Port K pin [A8,A9,...A15].

   The function "processRE()" code is adapted from Oleg Mazurov (2011 Mar 30)
   Ref: www_circuitsathome.com/mcu/rotary-encoder-
                interrupt-service-routine-for-avr-micros/

   This code uses 2 ISR's (one per port) to sense port interrupts on the two Mega ports.

   A one detent turn of an encoder takes either the "A" or "B" pin LOW (ground)
   which triggers the respective Arduino port interrupt. That interrupt triggers
   the Arduino ISR code which reads the port pins and sets flags for further
   processing to determine the encoder direction of turn and the number of turns.
   Each port interrupt is processed independently of other interrupts.

   Ref: thewanderingengineer.com/2014/08/11/arduino-pin-change-interrupts/
   Ref: www_gammon.com.au/forum/?id=11130

   For use as USB-HID joyStick:
   1) connect potentiometer wiper pins to analog pins A0, A1, A2
   2) In loop(), comment out "sendFlag = 0;" line to use axes
   3) connect rotary encoders to Mega digital pins
   4) test with DEBUG to check joyReport data are correct
   5) #undef DEBUG so that joyReport data are not corrupted with Serial.print
   6) program USB ATmega16U2 as USB-HID

   ======================================================

   Hardware Wiring Example: KY040 Rotary Encoder #0, pin CLK "A" wired
      to Ard pin 53 (on port B), and Rotary Encoder #0, pin DT "B" wired
      to Ard pin A8 (on port K).

   Arduino digital pins are set as pin-change-interrupt INPUT_PULLUP
      which trigger the port interrupt when state changes HIGH-LOW or LOW-HIGH.

   A clockwise (CW) turn of a Rotary Encoder evaluates to -1.
   A counter-clockwise (CCW) turn of a Rotary Encoder evaluates to 1.

   Switches are wired to pins 34-49 to ground and will display as joystick
   pins 16-31. Switches can also be wired to hardware interrupt pins 21 and 20,
   however those switches will not be sensed if used with FSX.

   ======================================================

*/

// to turn on DEBUG, define DEBUG. Make sure to undef DEBUG for joystick use
//#undef DEBUG
#define DEBUG

// ======================================================
// Sketch Code

const byte ENCODERS = 8;        // the number of rotary encoders, 4 per pciPort
const byte NUM_BUTTONS = 40;    // do not change this value.
const byte NUM_AXES = 8;        // do not change this value.

// ======================================================
// add 2 hardware interrupts
const byte HWINTS = 2;
uint8_t hwArray[HWINTS] = {21, 20};
uint8_t hwFlag[HWINTS];

// ======================================================
// add 16 switches
const byte SWITCHES = 16;
uint8_t swArray[SWITCHES] = {34, 35, 36, 37, 38, 39, 40, 41,
                             42, 43, 44, 45, 46, 47, 48, 49
                            };
uint8_t swFlag = 0; // switch data placed into joyReport flag

// ======================================================
// define joyReport
typedef struct joyReport_t {
  int16_t axis[NUM_AXES];
  uint8_t btnArray[(NUM_BUTTONS + 7) / 8]; // 8 buttons per byte
} joyReport_t;
joyReport_t joyReport;
joyReport_t prevjoyReport;
uint8_t sendFlag = 0; // axis data placed into joyReport flag

// ======================================================
// add 8 encoders
volatile uint8_t reFlag = 0;          // encoder data placed into joyReport flag
volatile uint8_t pciPortReadFlag [3]; // flag if pciPort was read
volatile uint8_t pciPortRead [3];     // save the port interrupt pin value byte

// create the ENCODER struct
typedef struct
{
  int reAPin;     // which RE pin for the "A" side interrupt
  int reBPin;     // which RE pin for the "B" side interrupt

  byte aBitMask;   // which interrupt bit in the A port
  byte bBitMask;   // which interrupt bit in the B port
  byte pciPortIntA;  // which pin-change interrupt port (0, 1, 2)
  byte pciPortIntB;  // which pin-change interrupt port (0, 1, 2)

  volatile uint8_t reAB;       // retained value
  volatile int8_t reValue;       // current RE value -1, 1
  volatile int32_t reCounter;   // running count for this re[0,1]
  volatile uint8_t reAPinGPIO;   // current value reAPin
  volatile uint8_t reBPinGPIO;   // current value reBPin
  //volatile uint8_t reAPinPrev;   // previous value reAPin
  //volatile uint8_t reBPinPrev;   // previous value reBPin
  volatile uint8_t reAFlag;      // reAFlag
  volatile uint8_t reBFlag;      // reBFlag

} RE;

// Use these arrays for encoder A pins on Port B and B pins on Port K interrupts
volatile RE re[ENCODERS] = {
  {53, A8},
  {52, A9},
  {51, A10},
  {50, A11},
  {10, A12},
  {11, A13},
  {12, A14},
  {13, A15},
}; // end of encoders


// Mega 2560 External Interrupt pins, 2, 3, 18, 19, 20, & 21

// Mega 2560 Pin-Change-Interupt Ports and pins
// PB7...PB4 DPins 13...10 and PB3...PB0 DPins 50...53 (PCINT7...0)
// PK7...PK0 APins A15...A8 (PCINT23...16)
// PJ1, PJ0  DPins D14, D15 not used

// 8 Rotary Encoders are possible with 16 PCI pins across two 8-pin ports PB and PK
// Mega 2560 PCI pins: PB7...PB0 D13...D10 & D50...D53 (no SPI possible)
// Mega 2560 PCI pins: PK7...PK0 A15...A8

// ++++++++++++++++++++++++++++++++++++++++++++++++++++


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
//   function prototypes - not necessary but show the
//         functions used in this sketch
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// pin change interrupts
void checkForPinChange (const byte pciPort);
ISR (PCINT0_vect);
//ISR (PCINT1_vect);
ISR (PCINT2_vect);
void hw0Interrupt();
void hw1Interrupt();
void setup();
void sendJoyReport(struct joyReport_t *report);
void processRE(uint8_t pciPort);
void loop ();
void print8Bits(uint8_t myByte);
void print16Bits(uint16_t myWord);
void crPrintHEX(unsigned long DATA, unsigned char numChars);

// ++++++++++++++++++++++++++++++++++++++++++++++++++++


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
//   function checkForPinChange()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// Rotary encoder pin change triggers "pciPort" Arduino ISR.
// pciPort==0 is Ard Port B, pciPort==1 is not used, pciPort==2 is Ard Port K.
// Once the ISR finishes, interrupts are cleared, then loop() processes the pins.
// The pin data are written to the joyReport data structure for sending out USB.

void checkForPinChange (const byte pciPort)
{
  //Serial.print ("\n\n =============== pciPort: "); Serial.print (pciPort);
  //Serial.print ("\n checkForPinChange ");

  if (pciPort == 0) {
    pciPortRead [pciPort] = ~PINB;
    pciPortReadFlag [pciPort] = 1;
  } else if (pciPort == 1) {
    pciPortRead [pciPort] = 0b0;
    pciPortReadFlag [pciPort] = 1;
  } else if (pciPort == 2) {
    pciPortRead [pciPort] = ~PINK;
    pciPortReadFlag [pciPort] = 1;
  }

  for (uint8_t reID = 0; reID < ENCODERS; reID++) {
    if (re[reID].pciPortIntA == pciPort) {
      if ((digitalRead(re[reID].reAPin) == LOW)) {
        re[reID].reAFlag = 1;
#ifdef DEBUG
        /*
        // print what pins are changing and in what order of interrupt
        Serial.print ("\n, reAFlag: "); Serial.print (re[reID].reAFlag);
        Serial.print (", pciPortRead [pciPort]: "); print8Bits (pciPortRead [pciPort]);
        */
#endif
      }
    }
    if (re[reID].pciPortIntB == pciPort) {
      if ((digitalRead(re[reID].reBPin) == LOW)) {
        re[reID].reBFlag = 1;
#ifdef DEBUG
        /*
        // print what pins are changing and in what order of interrupt
        Serial.print ("\n, reBFlag: "); Serial.print (re[reID].reBFlag);
        Serial.print (", pciPortRead [pciPort]: "); print8Bits (pciPortRead [pciPort]);
        */
#endif
      }
    }
  }     // end of for each encoder
} // end of checkForPinChange

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
//   ISR (PCINT0_vect)
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// handle pin change interrupt for Mega 2560 pins 53-50 & 10-13
ISR (PCINT0_vect)
{
  cli(); //stop interrupts happening before we read pin values
  checkForPinChange (PCIE0);
  sei(); //restart interrupts
}  // end of PCINT0_vect


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
//   ISR (PCINT1_vect)
// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// handle pin change interrupt not used
//ISR (PCINT1_vect)
//{
//  cli(); //stop interrupts happening before we read pin values
//  checkForPinChange (PCIE1);
//  sei(); //restart interrupts
//}  // end of PCINT1_vect

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
//   ISR (PCINT2_vect)
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// handle pin change interrupt for Mega 2560 pins A8-A15
ISR (PCINT2_vect)
{
  //Serial.println ("ISR (PCINT2_vect) ...");
  cli(); //stop interrupts happening before we read pin values
  checkForPinChange (PCIE2);
  sei(); //restart interrupts
}  // end of PCINT2_vect

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
//   hw0Interrupt()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// hwArray[0] pin triggers this hardware interrupt
void hw0Interrupt()
{
  //Serial.println ("hw0Interrupt() ...");
  cli(); //stop interrupts happening before we read pin values
  hwFlag[0] = 0; // clear the flag
  if (digitalRead(hwArray[0] < 1)) { // pin is LOW
    hwFlag[0] = 1; // set the flag
  }
  sei(); //restart interrupts
}  // end of hw0Interrupt()

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
//   hw1Interrupt()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// hwArray[1] pin triggers this hardware interrupt
void hw1Interrupt()
{
  //Serial.println ("hw1Interrupt() ...");
  cli(); //stop interrupts happening before we read pin values
  hwFlag[1] = 0; // clear the flag
  if (digitalRead(hwArray[1] < 1)) { // pin is LOW
    hwFlag[1] = 1; // set the flag
  }
  sei(); //restart interrupts
}  // end of hw1Interrupt()

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function setup
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void setup() {

  Serial.begin (115200);

#ifdef DEBUG
  Serial.println(F("\n Starting Mega 2560 Pin-Change-Interrupt Joystick"));
#endif

  //----------------------------------------------------------
  // Arduino pin-change-interrupts configuration
  //----------------------------------------------------------
  //
  // clear any outstanding Arduino pin-change-interrupts
  PCIFR  |= bit (PCIF0) | bit (PCIF1) | bit (PCIF2);

  for (uint8_t reID = 0; reID < ENCODERS; reID++)
  {

    // set interrupt pins to INPUT_PULLUP
    pinMode (re[reID].reAPin, INPUT_PULLUP);
    pinMode (re[reID].reBPin, INPUT_PULLUP);

    // Create a Mask with the pin bit = 1
    re[reID].aBitMask = digitalPinToBitMask (re[reID].reAPin);
    re[reID].bBitMask = digitalPinToBitMask (re[reID].reBPin);

    // Which ISR for each interrupt pin (0=PCIE0, 1=PCIE1, 2=PCIE2)
    re[reID].pciPortIntA = digitalPinToPCICRbit (re[reID].reAPin);
    re[reID].pciPortIntB = digitalPinToPCICRbit (re[reID].reBPin);

    // Activate this pin-change interrupt bit (eg. PCMSK0, PCMSK1, PCMSK2)
    /*   Ref: thewanderingengineer.com/2014/08/11/arduino-pin-change-interrupts/
      // Example PCMSK definitions:
      PCMSK0 |= 0b00000011;    // turn on pins PB0 & PB1, PCINT0 & PCINT1, pins D8, D9
      PCMSK1 |= 0b00010000;    // turn on pin PC4, pciPort is PCINT12, pin A4
      PCMSK2 |= 0b00001100;    // turn on pins PD2 & PD3, PCINT18 & PCINT19, pins D2, D3
    */

    PCMSK0 |= 0b11111111;    // turn on port b pins PCINT0 to PCINT7
    PCMSK1 |= 0b00000000;    // not used
    PCMSK2 |= 0b11111111;    // turn on port k pins PCINT16 to PCINT23

    // Enable this pin-change interrupt
    /*   Ref: thewanderingengineer.com/2014/08/11/arduino-pin-change-interrupts/
      // Example PCICR definitions:
      PCICR |= 0b00000001;    // turn on port b
      PCICR |= 0b00000010;    // turn on port c
      PCICR |= 0b00000100;    // turn on port d
      PCICR |= 0b00000111;    // turn on all ports
    */
    PCICR |= bit (digitalPinToPCICRbit (re[reID].reAPin));
    //Serial.print ("\nPCICR A: "); print8Bits (PCICR);
    PCICR |= bit (digitalPinToPCICRbit (re[reID].reBPin));
    //Serial.print (", PCICR B: "); print8Bits (PCICR);

#ifdef DEBUG
    // examine the Arduino ISR setup
    Serial.print ("\n\nMega 2560: ");
    Serial.print ("\n...re[reID].reAPin: "); Serial.print (re[reID].reAPin);
    Serial.print (", re[reID].reBPin: "); Serial.print (re[reID].reBPin);
    Serial.print (", re[reID].aBitMask: "); print8Bits(re[reID].aBitMask);
    Serial.print (", re[reID].bBitMask: "); print8Bits(re[reID].bBitMask);
    Serial.print ("\n...re[reID].pciPortIntA: "); Serial.print (re[reID].pciPortIntA);
    Serial.print (", re[reID].pciPortIntB: "); Serial.print (re[reID].pciPortIntB);
    Serial.print (", PCICR: "); print8Bits (PCICR);
    Serial.print (", PCMSK0: "); print8Bits (PCMSK0);
    Serial.print (", PCMSK1: "); print8Bits (PCMSK1);
    Serial.print (", PCMSK2: "); print8Bits (PCMSK2);
#endif

    // initalize the 8 encoders on each port
    for (uint8_t reID = 0; reID < ENCODERS; reID++) {
      //re[reID].reAPin = RE pin for the "A" side interrupt
      //re[reID].reBPin = RE pin for the "B" side interrupt
      re[reID].reAB = 3;         // old_AB
      re[reID].reValue = 0;      // current RE value -1, 1
      re[reID].reCounter = 0;    // running count for this re[0,1]
      re[reID].reAPinGPIO = 0;   // current value reAPin
      re[reID].reBPinGPIO = 0;   // current value reBPin
      //re[reID].reAPinPrev = 0;   // previous value reAPin
      //re[reID].reBPinPrev = 0;   // previous value reBPin
      re[reID].reAFlag = 0;      // reAFlag
      re[reID].reBFlag = 0;      // reBFlag
    } // end of setup encoders

  } // end of Arduino interrupts for each expander

  // -------------------------------------------------
  // set up two hardware interrupt pins (Mega 2560 = 21, 20, 19, 18, 2, 3)
  for (uint8_t hw = 0; hw < HWINTS; hw ++) {
    pinMode (hwArray[hw], INPUT_PULLUP);
    hwFlag[hw] = 0; // clear flag
  }
  attachInterrupt(digitalPinToInterrupt(hwArray[0]), hw0Interrupt, CHANGE);
  attachInterrupt(digitalPinToInterrupt(hwArray[1]), hw1Interrupt, CHANGE);

  // -------------------------------------------------
  // set up 16 switch pins
  for (uint8_t sw = 0; sw < SWITCHES; sw++) {
    pinMode (swArray[sw], INPUT_PULLUP);
  }

}  // end of setup


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function sendJoyReport()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// Send an HID report to the USB interface
void sendJoyReport(struct joyReport_t *report)
{
#ifndef DEBUG
  //Serial.write((uint8_t *)report, sizeof(joyReport_t));
  // do not send duplicate values
  //
  if (memcmp( report, &prevjoyReport, sizeof( joyReport_t ) ) != 0)
  {
    Serial.write((uint8_t *)report, sizeof(joyReport_t));
    memcpy ( &prevjoyReport, report, sizeof( joyReport_t ) );
  }
  //
  // end do not send duplicate values
#else
  if (memcmp( report, &prevjoyReport, sizeof( joyReport_t ) ) != 0)
  {
    //Serial.write((uint8_t *)report, sizeof(joyReport_t));
    memcpy ( &prevjoyReport, report, sizeof( joyReport_t ) );
    //}
    // dump human readable output for debugging
    Serial.println("\n");
    for (uint8_t ind = 0; ind < NUM_AXES; ind++) {
      Serial.print("axis[");
      Serial.print(ind);
      Serial.print("]= ");
      Serial.print(report->axis[ind]);
      Serial.print(" ");
    }
    Serial.println();
    for (uint8_t ind = 0; ind < NUM_BUTTONS / 8; ind++) {
      Serial.print("btnArray[");
      Serial.print(ind);
      Serial.print("]= ");
      print8Bits(report->btnArray[ind]);
      //Serial.print(report->btnArray[ind], HEX);
      Serial.print(" ");
    }
  } else {
    //Serial.print("\n ...No Change in joyReport...");
  }

#endif
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function processRE()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void processRE(uint8_t pciPort) {

  /*
   * Process the encoders on each expander
   * 4 ENCODERS per Arduino Port Interrupt
   *
      for (uint8_t reID = 0; reID < ENCODERS; reID++) {
        re[reID].reAPin = read array;    // which RE pin for the "A" side interrupt
        re[reID].reBPin = read array;    // which RE pin for the "B" side interrupt
        re[reID].reValue = 0;      // current RE value -1, 1
        re[reID].reCounter = 0;    // running count for this re[0,1]
        re[reID].reAPinGPIO = 0;   // current value reAPin
        re[reID].reBPinGPIO = 0;   // current value reBPin
        re[reID].reAPinPrev = 0;   // previous value reAPin
        re[reID].reBPinPrev = 0;   // previous value reBPin
        re[reID].reAFlag = 0;      // reAFlag
        re[reID].reBFlag = 0;      // reBFlag
      } // end of setup encoders

  */

  // ++++++++++++++++++++++++++++++++++++++++++++++++++++
  // this code is adapted from Oleg Mazurov (2011 Mar 30)
  // Ref: www_circuitsathome.com/mcu/rotary-encoder-
  //            interrupt-service-routine-for-avr-micros/
  // ++++++++++++++++++++++++++++++++++++++++++++++++++++

  // This code determines the direction of turn and cumulative count
  //     for up to 8 rotary encoders on 1 Arduino Mega 2560 using 2 8-pin ports.
  // A turn of a rotary encoder on a port
  //     causes a series of pin change interrupts
  //     that triggers this code for each interrupt.
  // For the KY040 Rotary Encoder:
  //    pin CLK ==> RE Pin reAPin, pin DT ==> RE Pin reBPin
  // The reAPin goes to port B [0...7] pins (53, 52, 51, 50, 10, 11, 12, 13)
  // The reBPin goes to port K [0...7] pins (A8, A9, A10, A11, A12, A13, A14, A15)
  // This arrangement gives a separate port interrupt for each pin of an encoder

  static const int8_t enc_states [] PROGMEM =
  {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0}; //encoder lookup table

  // cycle through each of 8 rotary encoders
  // with pin A on Port B and pin B on Port K
  for (uint8_t reID = 0; reID < ENCODERS; reID++) {

    // recall retained value of old_AB
    uint8_t old_AB = re[reID].reAB;
    uint8_t encport = 0;
    int8_t dir;

    // get the expander:encoder:pinA, pinB values at time of interrupt
    // eg, Port_B [0] & Port_K [0], Port_B [5] & Port_K [5]

    // for encoders with pin A on Port B and pin B on Port K
    re[reID].reAPinGPIO = bitRead(pciPortRead [re[reID].pciPortIntA], reID);
    re[reID].reBPinGPIO = bitRead(pciPortRead [re[reID].pciPortIntB], reID);

    // check if this RE sent a signal, if not, ignore it
    if ((re[reID].reAPinGPIO > 0) || (re[reID].reBPinGPIO > 0)) {
      old_AB <<= 2; //remember previous RE state and shift-left by 2 bits
      // copy encoder pin values to encport bits 1,0
      if (re[reID].reAPinGPIO > 0) bitSet(encport, 0);
      if (re[reID].reBPinGPIO > 0) bitSet(encport, 1);
      //copy bits 1,0 to old_AB
      old_AB |= encport & 0x03;
      // use index to obtain direction and state
      dir = pgm_read_byte(&(enc_states[( old_AB & 0x0f )]));
      //check if at detent and transition is valid
      // dir=1 CCW, dir=-1 CW
      if ( dir && ( encport == 3 )) {
        re[reID].reValue = dir;
        re[reID].reCounter = re[reID].reCounter - dir;

        reFlag = 1; // joyReport data changed flag
        // place the 8-encoder data into
        //       joyReport [ 0 and 1 ]

        if (dir < 0) bitSet(joyReport.btnArray[1], reID); // CW data
        if (dir > 0) bitSet(joyReport.btnArray[0], reID); // CCW data

        // btnArray[2] and btnArray[3] are available for use with 16 switches
        // connected to other Mega 2560 pins (code has to be added)

#ifdef DEBUG

        Serial.print ("\n RE["); Serial.print (reID); Serial.print ("]");
        Serial.print (", encport: "); print8Bits (encport);
        Serial.print (", old_AB: "); print8Bits (old_AB);
        Serial.print (", enc_states["); Serial.print (old_AB & 0x0f); Serial.print ("]: ");
        Serial.print (" dir: "); Serial.print (dir);
        if ( dir == 1 ) {
          Serial.print (", CCW");
        }
        else {
          Serial.print (", CW");
        }
        Serial.print (", Count "); Serial.print ( re[reID].reCounter);
#endif
      } // end if (dir...
    } // end if ((re[reID].reAPinGPIO...

    // retain settings for this encoder for next indent comparison
    re[reID].reAB = old_AB;

  } // end encoder  for (uint8_t reID...

} // end code adapted from Oleg Mazurov (2011 Mar 30)


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function loop
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void loop ()
{

  // This code runs Arduino Mega 2560 pin-change-interrupt ISR's
  //    and then takes action (checkForPinChange()) for the Arduino pins that
  //    went LOW or HIGH. Port B and Port K pins are set as pin-change-interrupts.

  // checkForPinChange() flags that a pin interrupt (re[reID].reAFlag>0) has occurred
  //    and each pciPortReadFlag[pciPort] is set if a port pin
  //    changed state.

  // checkForPinChange() reads the inverse value of the port pins
  //   (pciPortRead[pciPort] = ~PINB;) and  (pciPortRead[pciPort] = ~PINK;)
  // processRE(pciPort) writes the encoder values into joyReport[0], [1].

  // process Rotary Encoders
  for (uint8_t pciPort = 0; pciPort < 3; pciPort++) {
    if (pciPortReadFlag [pciPort] > 0) {
      processRE(pciPort);
    }
  }

  // process the switches for ON and OFF
  swFlag = 1;

  // process switches attached to hardware interrupts
  for (uint8_t hw = 0; hw < HWINTS; hw++) {
    if (hwFlag[hw] > 0) { // test if interrupt occurred with pin LOW
      if (digitalRead(hwArray[hw]) < 1) {  // check if pin is still LOW
        bitSet(joyReport.btnArray[4], hw); // switch is ON
      }
    }
  }

  // process 16 switches and place their values into joyReport[2], [3].
  // poll each switch to determine its state
  // sendJoyReport() will determine if there is a change in one or more switches
  uint8_t bt = 0; // bit number
  uint8_t pn = 2; // port number
  for (uint8_t sw = 0; sw < SWITCHES; sw++) {
    if (digitalRead(swArray[sw]) < 1) {
      if (sw < 8) {
        bt = sw; // 0...7
        pn = 2;
      } else {
        bt = sw - 8; // 8...15 ==> 0...7
        pn = 3;
      }
      bitSet(joyReport.btnArray[pn], bt);
    }
  }

  /* Axes connect to Analog pins A0, A1, A2...A7 */
  /* Arduino UNO has 6 analog pins of 8 possible. Set pin to 0 if not used */
  /* Ground any analog ports that are not connected to potentiometers to reduce noise */
  uint8_t axisCount = 0; // set the number of axes you want to use, 3=[0,1,2]
  for (uint8_t axis = 0; axis < axisCount; axis++) {
    int tmp = joyReport.axis[axis]; // copy previous axis value
    // Average 5 readings of port to get better values from noisy potentiometers
    // Use >5 to average more readings per pot
    long sumAxis = 0;
    int avg = 0;
    int count = 5;
    for (int i = 0; i < count; i++) {
      sumAxis = sumAxis + analogRead(axis);
    }
    avg = sumAxis / count;
    joyReport.axis[axis] = map(avg, 0, 1023, -32768, 32767 );

    // flag change in axis if avg reading changes by > 100
    if (abs(joyReport.axis[axis] - tmp) > 100) sendFlag = 1;
  }

  //Set un-used analog pins to 0 to reduce spurious values in joyReport.
  for (uint8_t i = axisCount; i < 8; i++) {
    joyReport.axis[i] = 0;
  }

  // for now, turn off axis data for testing digital inputs
  // comment out "sendFlag = 0;" line to use axes
  sendFlag = 0;

  if ((swFlag > 0) || (reFlag > 0) || (sendFlag > 0)) {
    //Send Data to HID
    sendJoyReport(&joyReport);

    // clear the joyReport
    sendFlag = 0;
    reFlag = 0;
    swFlag = 0;
    joyReport.btnArray[0] = 0b0;
    joyReport.btnArray[1] = 0b0;
    joyReport.btnArray[2] = 0b0;
    joyReport.btnArray[3] = 0b0;
    joyReport.btnArray[4] = 0b0;
  }
  // clear pci port read flags
  for (uint8_t pciPort = 0; pciPort < 3; pciPort++) {
    pciPortReadFlag [pciPort] = 0;
  }

  //}
  delay (10); // give loop something to do while idle

}

#ifdef DEBUG
// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// print binary8 binary16 and hex functions
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

//---------------------------------------------------------------------------------
// print 8-bit byte as 8 bit binary string
//---------------------------------------------------------------------------------

void print8Bits(uint8_t myByte) {
  for (uint8_t mask = 0x80; mask; mask >>= 1) {
    if (mask  & myByte)
      Serial.print('1');
    else
      Serial.print('0');
  }
}

//---------------------------------------------------------------------------------
// print 16-bit word as 2 8-bit bit binary strings with space
//---------------------------------------------------------------------------------

void print16Bits(uint16_t myWord) {
  print8Bits(highByte(myWord));
  Serial.print (" ");
  print8Bits(lowByte(myWord));
  /*
    for (uint16_t mask = 0x8000; mask; mask >>= 1) {
    if (mask  & myWord)
      Serial.print('1');
    else
      Serial.print('0');
    }
  */
}

//---------------------------------------------------------------------------------
// crPrintHEX print value as hex with specified number of digits
//---------------------------------------------------------------------------------

void crPrintHEX(unsigned long DATA, unsigned char numChars) {
  unsigned long mask  = 0x0000000F;
  mask = mask << 4 * (numChars - 1);
  Serial.print("0x");
  for (unsigned int eID = numChars; eID > 0;  --eID) {
    Serial.print(((DATA & mask) >> (eID - 1) * 4), HEX);
    mask = mask >> 4;
  }
  Serial.print("  ");
}

#endif


(February 2, 2017)