Shop OBEX P1 Docs P2 Docs Learn Events
Program Architecture Best Practices with Multiple Objects and Cogs — Parallax Forums

Program Architecture Best Practices with Multiple Objects and Cogs

SteveWoodroughSteveWoodrough Posts: 190
edited 2012-11-25 19:42 in Propeller 1
Happy Thanksgiving Everyone
After three years of study on and off I’m finally on the cusp of doing something interesting with a Robo-Magellan type bot. As an introduction, I came to the Prop from the BS2 as a 45 year old mechanical engineer with limited programming experience and nothing in the way of familiarity with object oriented programming. Hanno’s 12Blocks was a confidence builder, and the Kick Start book an epiphany. I understand at a beginners level how to do the following each within a one or two level object set. For example, I can from my own “Top” object, send messages to an LCD using the Full Duplex Serial library object, drive a servo using the servo32V7 object, control a speed controller with PID feedback, get compass data, get GPS data, etc. I feel like I have a fundamental “tool box” to finally get started and integrate my Robo-Magellan project. I want to build the right foundation to support the project that will require multiple objects and multiple cogs.

My questions are: What are the best practices to structure a program that requires multiple objects and multiple cogs? I understand that there may be no one single best practice, since each application may have its own demands. What is best for one project may not be the best for another. Generally speaking, should the Top object reference all relevant objects required for the project, excluding objects referenced by an object for its own use? An example of the exclusion would be the need to reference only Servo32V7 in the top object and not Servo32_Ramp_v2 since the latter is referenced by the former.

Likewise is a best practice that all I/O pin assignments etc. be in the Top object or distributed within referenced objects?

That is the short version of my question. Below is an abbreviated description of what I want to accomplish and the knot of concepts I’ve formed in my thinking.

Thank You for your time and responses.

Regards,
Steve

What I want to avoid is a Main / Top that becomes some galactic wad of methods. In my mind, and very possibly an errant fantasy, the Top object should consist of a single Main method that is set of instructions and likely in the form of a repeating loop. In pseudo code the Top object might look something like this:

Pub Main
Repeat
Do you want to calibrate compass? Y/N
Compass calibrated
Enter Heading
Enter Distance
Repeat
Bot Ready to go? Y/N
Bot Ready, so go distance at heading
Run Complete
Bot stops
Enter Heading
Enter Distance
Time for Dinner (leave loop)

In this architecture plan all the details about driving servos (separate cog), sending messages to LCD’s (separate cog), monitoring keypads, monitoring encoder pulses (separate cog), monitoring compass data, I/O pin assignments etc. are accomplished in separate dedicated objects. This is all done in the interest of keeping everything neat and tidy and compartmentalizing functions within the relevant objects. Servo stuff in servo objects, LCD display stuff in LCD objects, and so on. Continuing down an object level in this architecture plan, the LCD display pin assignment is part of an LCD object and the LCD object references and Starts the Full Duplex Serial (FDS) object in its own cog.

Is the above description “a wolf in sheep’s wool”? By embedding FDS in a lower object, dedicated to the LCD function, have I completely cut myself off from using the functionality of FDS elsewhere in the overall program even though FDS is started and is running in its own cog? Could I, in another object, somehow functionally use the expression FDS.tx(128) to more the cursor to the first position on the LCD screen? My own experiments suggest that I cannot, but maybe there is something I have not yet learned.

I suppose that the LCD object, that it self references the FDS object, could be written with methods that referenced the functionality of the FDS object. With that approach parameters are passed from any object to the appropriate corresponding LCD object method which then go to the FDS object running in its own cog. It just seems a bit extreme to have to write methods referencing methods that already exist in the referenced object. On further reflection, passing data strings such as “Hello World” as parameters to access a method such as: FDS.str(string("Hello World")) may be impossible or so convoluted as to negate the intended purpose of simplicity.

Comments

  • Mike GreenMike Green Posts: 23,101
    edited 2012-11-22 10:00
    Typically, the pin assignments are declared in the main object and passed to the child objects using their .start methods where a local copy is stored for use later. This is used with FDS and SD card support objects as well as others. The reason for this is that the main object is the overall project object and has to "know" this sort of stuff. I've used other schemes. For example, DongleBasic (from the Object Exchange) uses a separate definitions object that has all sorts of definitions in it. These can be referenced from the main object as def#... references which are then passed to other child objects using their .start methods.

    Don't get hung up on having the top object just containing the main method. By moving everything else to "lower" objects, you may be needlessly complicating your program when it might make much more sense to have the whole upper levels of your program in a single object. Objects are really intended to encapsulate some kind of abstract functionality. Most of the time, these are I/O drivers where the interface methods provide the abstraction to the rest of the world and, internally, the object may use all sorts of resources including internal variables and cogs that the "outside world" doesn't have to know about. If done properly, the "outside world" doesn't have to know whether other cogs are used or how much and what kinds of memory are used. It all looks the same to the caller (or should) whether the I/O driver is actually written in PASM or Spin or C for that matter.

    Normally when you reference an object in another object (like: OBJ FDS:"FullDuplexSerial"), a new instance of that object is created complete with its own copy of the object's local variables. This is different from any other declaration of an object with the same file name. Multiple object instances share the same code bytes since those are read-only and they share any DAT areas which are usually PASM code ... also read-only. It's possible to have multiple instances of an object that keep all their variables in DAT sections so that's shared and there are such objects in the Object Exchange so multiple objects can use them for common I/O.
  • SRLMSRLM Posts: 5,045
    edited 2012-11-22 10:23
    For general purpose programming, I recommend the book "The Pragmatic Programmer": http://www.amazon.com/Pragmatic-Programmer-Journeyman-Master/dp/020161622X It's a handbook of good coding practices.

    Now, on to your specific questions:

    In general, I like to structure my program with a 1:1 mapping between cogs and objects. The way this works out is usually where only the top level object has sub objects. Still, as long as the organization makes sense and is clear then whatever method you use is fine.

    I like to define all pins in the top level object. That way, you can order them all numerically and make sure that there are no conflicts, and this design makes it easy to change hardware. I make each pin a constant.
  • SteveWoodroughSteveWoodrough Posts: 190
    edited 2012-11-22 17:43
    Mike and SRLM,
    Thank you both for your time and respones. As I prepared my question, I began to reach the same conclusion, but I wanted to be sure I had the correct perspective. I dreaded laying down some kind of uninformed foundation only to discover later in the process that I would need to dig it all up and begin again.
    Best Regards,
    Steve
  • SteveWoodroughSteveWoodrough Posts: 190
    edited 2012-11-23 19:07
    Continuing the advice from above below is my current code for my RoboMagellan Project. I’ve appended this new question to the original answers since the current problem is related to the original question I asked.

    Functionally everything so far works individually, but as an integrated program, I'm missing some concept. The program executes the LCD_CruiseControl method but never gets down to the BlinkLED method referenced in the main method.

    The assembled parts so far consist of driving a RC truck motor controller that is using PID feedback to control its speed. Both servo32V7 object and the CruiseControl (PID) method run in two separate cogs, and work just great. FDS also runs in a separate cog and sends the data to my LCD screen just fine. The BlinkLED method works as long as the LCD method is commented out. For some reason the cog that the Main method is running is stuck in the repeat loop of the LCD method. I thought that since FDS is running in a separate cog, this repeat loop would be contained in the cog running FDS.

    Looking at the code, I can sense a subtle difference in how I started the CruiseControl method in its own cog, but LCD_CruiseControl is running in the starting cog, even though FDS is running in a separate cog, via FDS.start.

    What's the fix? Do I start another cog to execute the LCD_CruiseControl method, just to send one message? At the rate I’m using cogs, I’ll need 32! LOL!

    Seriously though, how does one best avoid “Cog Clogs”? Am I correct to start FDS.start as part of an initialization scheme, or should I wait, to start FDS only when I want to send a message to the LCD then shut the cog down with FDS.stop? If I want messages to stream to the LCD independent of other processes, do I need two cogs, one to run FDS and a second to send the commands to the LCD?

    Lastly, If I need to start and stop a separate cog to handle the LCD message fuctions do I need a Stop method independant of the Stop method referenced by my CruiseControl method?

    Thanks
    Steve
    {**************************************************
    *       RoboMagellan Version 1.0                  *
    *                                                 *
    ***************************************************
    This is to be the top object for a Robo Magellan type project.
    
    }
    CON
     _clkmode        = xtal1 + pll16x
     _xinfreq        = 5_000_000
    
    'Pin Assignements:
    BTTx    =  0  'Tx Pin for Blue Tooth Module run in View Port
    BTRx    =  1    'Rx Pin for Blue Tooth Module run in View Port 
    
    TachPin =  2  'Input from Hall Effect sensor to sense motor speed 
    MotorPin = 3  'Output Pin connection for the RC Truck Motor Controller
    LCD_Pin  = 4  'Output Pin for LCD display
    SteerPin = 5  'Output Pin connection for the RC Truck Servo
    
    SDDO     = 8  'SDCard DO
    SDCL     = 9  'SDCard SCLK
    SDDI     = 10 'SDCard DI
    SDCS     = 11 'SDCard CS
    
    GPS_Pin  = 15 'Input from GPS module
    
    CMCL     = 23 'SCL for HMC6352 Compass
    CMDA     = 24 'SDA for HMC6352 Compass
    
    'Other Constants
    TachG = 30      'Tachometer Goal Used by Cruise Control Method Desired speed
    TachD = 100     'Duration of Tachometer Monitoring in mSec
    
    MotorSpeedMin = 1100
    MotorSpeedMax = 1250
    
    Kp       = 30     'Proprotional Gain
    Ki       = 1      'Integral Gain    
    Kd       = -35    'Differential Gain         
    
    Baud    =19_200   'Baud Rate for FDS object
     
    VAR
      long MotorSpeed                        'Used by Cruise Control Method
      long TachCount                         'Used by Cruise Control Method
    
      long integratedError,oldError          'Used by PID Routine
    
      long          CCStack[10]             'stack space for CruiseControl method 
      byte          Cog                      'Stores the ID of new cog
    
    OBJ
                                              'COG (0)  Main Method, Cog(N) is my notation to keep track of potential Cogs
                                              'COG (4)  CruiseControl Started in Main method below  
    vp:             "Conduit"                 'SEPARATE COG (1) used to communicate with View Port             
    servo:          "servo32v7"               'SEPARATE COG (2)Standard library object for servo type commands 
    
    FDS:             "FullDuplexSerial"       'SEPARATE COG (3) Standard library object used with LCD and GPS  
    Comp:            "HMC6352"                'Object reads I2C compass data from HMC6352 Compass module     
    
    'MGPS:           "MagellanGPS"            'Future object obtains and parse GPS data
    'MSD:            "MagellanSDCard"         'Future object read and write to SD card                               
                                           
    
    Pub Main  |a,b
    'ININTIALIZATION Routines below 
     vp.config(string("start:dso")) 'start in the dso mode
    'vp.register                    'Future use with VP
     vp.share(@a,@b)                'Starts shareing data with VP
     vp.rxtx(0,1)                   'txrx from the BT Module
     servo.start                    'Starts Servo object in its own cog.  
     FDS.start(LCD_PIN,LCD_PIN, %1000,Baud)      'Starts FDS Object in its own cog
     waitcnt(clkfreq / 100 + cnt)                ' Pause for FullDuplexSerial.spin to initialize 
    
    'Actual start of program method calls 
    StartCruiseControl               'Starts CruiseControl Method running in separate COG
    
    LCD_CruiseControl                'Starts method that should be running in separate COG
    
    BlinkLed(16)                     'Represents a continuing operation to ensure control returned back to Main Method 
     
    
    Pub LCD_CruiseControl           'Display Cruise Control Settings and results on LCD
    
      FDS.tx(12)                    'Clear Screen     
    
      FDS.tx(17)                    'Turn on Back Light   
    Repeat
      
      FDS.tx(128)                   'Move Cursor to first postion
      FDS.str(string("Tach Goal "))
      FDS.dec(TachG)
      FDS.tx(148)                   'Move Cursor to second row 
      FDS.str(string("Tach Count     ")) 'Extra spaces clears old data from display
      FDS.tx(159)                    'Position cursor in correct location
      FDS.dec(TachCount)             'Display Tach Data
      FDS.tx(168)                   'Move Cursor to third row 
      FDS.str(string("Motor Speed "))
      FDS.dec(MotorSpeed)
    
    PUB StartCruiseControl
     Stop                                           'Prevent multiple starts
     Cog := cognew(CruiseControl(TachG,TachD), @CCStack)   'Launch method in separate COG
    
    PUB Stop   '
    if Cog
       cogstop(Cog~ -1)                            'Stop previously launched cog
    
    pub CruiseControl(TachGoal,duration)  
      repeat 
           TachCount:=COUNT(TachPin,duration) 
           MotorSpeed+=dopid(TachGoal-TachCount,Kp,Ki,Kd)  
           MotorSpeed:=MotorSpeedMin #> MotorSpeed<# MotorSpeedMax  'SAFETY lower and upper limit of speed controller input 
           servo.set(MotorPin,MotorSpeed)
    
    PUB PAUSE(Duration) | cntS  'Taken from BSLite
       cntS:=cnt
    
       repeat ||Duration
        waitcnt(cntS+=clkfreq/1000)
    
    PUB COUNT (Pin, Duration) : Value                          'Adapted from BS.spin.  
    
           dira[PIN]~                                          ' Set as input
           ctra := 0                                           ' Clear any value in ctra
                                                               ' set up counter, pos edge trigger
           ctra := (%01010 << 26 ) | (%001 << 23) | (0 << 9) | (PIN)  
           frqa := 1                                           ' 1 count/trigger
           phsa := 0                                           ' Clear phase - holds accumulated count
           pause(duration)                                     ' Allow to count for duration in mS
           Value := phsa                                       ' Return total count
           ctra := 0                                           ' clear counter mode
           
    pub DoPID(error,p,i,d)|differentiatedError
      integratedError+=error
      differentiatedError:=oldError-error
      oldError:=error
      return (p*error+i*integratedError+d*differentiatedError)/100
    
    Pub BlinkLED (pin)
        dira[pin]~~                                          ' Set as Output     
      repeat 
        waitcnt (clkfreq/20 + cnt)
       ! outa[pin]                   'Toggle pin
    
    
    
    
  • Mike GreenMike Green Posts: 23,101
    edited 2012-11-23 19:25
    Generally you use cogs that have to run something in parallel with other things. FDS starts up a cog that does most of its work. Similarly, Servo32v7 starts up a cog to do most of the work. ViewPort requires a separate cog because of the speed required. I'm not sure why you might need a separate cog for CruiseControl. LCD_CruiseControl looks like it should be a part of CruiseControl. It sure looks like you need a main cog, one for FDS's assembly stuff, and one for Servo32v7's assembly stuff.
  • Tracy AllenTracy Allen Posts: 6,664
    edited 2012-11-24 00:45
    "The program executes the LCD_CruiseControl method but never gets down to the BlinkLED method referenced in the main method. "

    Both of those are spin methods that are being run in your main cog interpreter. Both of have repeat loops with no exit. So once your program hits one of them, it will stay there. If you want the blinkLED to happen too, there are a few ways to go about it. 1) include them both in one longer repeat main loop; 2) start a cog counter for the LED; 3) give the blinkLED or the LCD its own cog.

    "I thought that since FDS is running in a separate cog, this repeat loop would be contained in the cog running FDS. "

    No, while the machine language portion of FDS runs within a separate cog, both your LCD_CruiseControl and the Spin methods of FDS are being interpreted by the main cog. LCD_Cruise control sends variables to spin methods of FDS, which in turn put those variables into a buffer which is acted upon by the machine language component of FDS running in the other cog.

    "Am I correct to start FDS.start as part of an initialization scheme, or should I wait, to start FDS only when I want to send a message to the LCD then shut the cog down with FDS.stop? "

    Yes, initialize it, and no, it should not be necessary to shut it down. However, as you project develops, it sounds like you may need serial ports for several purposes. You have the LCD, but you also mentioned a GPS, and a compass, and maybe you also need a serial port for debugging direct to your computer during the process of development. In that case, you will need to open several serial ports. There are several ways to manage that, one of which is to start multiple instances of FDS.

    "If I want messages to stream to the LCD independent of other processes, do I need two cogs, one to run FDS and a second to send the commands to the LCD?"

    That would be one way to do it. There would be an interpreter cog running for the LCD commands and FDS spin, and a machine language cog for the pasm part of FDS. It should be possible to include other tasks such as the blinkLED in the loop with the LCD.

    "Lastly, If I need to start and stop a separate cog to handle the LCD message fuctions do I need a Stop method independant of the Stop method referenced by my CruiseControl method?"

    Yes. Each cog you start will need its own stop method and its own cogID variable.
  • SteveWoodroughSteveWoodrough Posts: 190
    edited 2012-11-24 16:26
    Mike Green wrote: »
    Generally you use cogs that have to run something in parallel with other things. FDS starts up a cog that does most of its work. Similarly, Servo32v7 starts up a cog to do most of the work. ViewPort requires a separate cog because of the speed required. I'm not sure why you might need a separate cog for CruiseControl. LCD_CruiseControl looks like it should be a part of CruiseControl. It sure looks like you need a main cog, one for FDS's assembly stuff, and one for Servo32v7's assembly stuff.

    Mike,
    Thank You for your response. What I’ve shown so far is only the basic start of the overall program. Within the program version I shown, I’m trying to understand just how things need to be to run as intended. What I FAILED to reveal in my earlier post was the need to monitor distance traveled via the same Hall Effect sensor. If I am not mistaken, continuously monitoring a sensor input is something I can only do if I run in parallel with the Main method.

    LCD could be part of the Cruise_Control method except there may be instances where I do not want to display the information on the LCD. I’m displaying the information now as part of an exercise to understand, but in the future, Cruise_Control should just run in the background monitoring the Hall Effect sensor.

    I suppose that before I ask for directions I should give a better idea on where it is I want to go. Below is a functional description / specification for my project. The first three items are included in the current program version. The remaining items are future functions that I intend to add one I get a handle on the matter currently under discussion.

    • Support the use of hobby grade servos and speed controllers.
    • Support an LCD display to display real time data of selected information depending upon the mode of the program. There will be multiple screen displays depending upon the operation performed. The LCD screen displayed at startup will be different than the display during compass calibration which will be different than the LCD displayed while traversing to a way point, etc.
    • Support the monitoring of actual motor speed via the Hall Effect sensor “Cruise_Control” so that the bot speed is some what constant over grass or pavement
    Future Functionality
    • Support the ability to determine distance traveled via the Hall effect sensor.
    • Support collision avoidance via PING sensor.
    • Support the use on an onboard compass module and its calibration at program start.
    • Support the use of GPS to determine location, speed, heading, etc.
    • Support un-tethered programming and real time monitoring of selected program variables. This is to be accomplished using VP and the Blue Tooth radio at some point. My use of the term “real time” may not be precise in engineering terms but its close enough for my purposes.
    • Support the use of an SD card reader to store and retrieve way point data.
    • Support the use of a 4x4 keypad to enter data as required.
  • SteveWoodroughSteveWoodrough Posts: 190
    edited 2012-11-24 16:59
    "The program executes the LCD_CruiseControl method but never gets down to the BlinkLED method referenced in the main method. "

    If you want the blinkLED to happen too, there are a few ways to go about it. 1) include them both in one longer repeat main loop; 2) start a cog counter for the LED; 3) give the blinkLED or the LCD its own cog.

    That would be one way to do it. There would be an interpreter cog running for the LCD commands and FDS spin, and a machine language cog for the pasm part of FDS. It should be possible to include other tasks such as the blinkLED in the loop with the LCD.

    Thank you Tracy for your time and consideration.
    BlinkLED was a bit of a red herring. BlinkLED was a crude representation of a Main method that would operate as a repeating loop in the primary cog. Therein would be contained all the If Then statements etc. for navigation, etc. Thanks to you and Mike, I see the error of my ways. I have a ton of questions, but feel I have enough information feel my way along to the next impass!

    Best Regards,
    Steve
  • Duane DegnDuane Degn Posts: 10,588
    edited 2012-11-24 17:47
    I only skimmed this thread so I apologize if this has already been mentioned.

    If you're using more than one instance of Full Duplex Serial, you may want to consider switching to a four port object. Tracy Allen's version is the best I know of.

    I haven't looked at the GPS objects for a while, but a while back (probably more than a year ago) I worked on a GPS data logger. I was disappointed how some of the GPS objects were structured. They started serial drivers in multiple places from various locations withing the object tree. These multiple serial drivers used cogs that weren't needed. I was able to trim away three cogs by moving code from several of the child objects to the top object. This made for a rather large top object buy it solved the problem of redundant serial objects.

    Make sure and take advantage of the "Summary" mode in the Propeller Tool. It makes large objects easier to navigate.

    Edit: There are lots of ways to blink a LED. I kind of like the object I made for this purpose. It lets you blink any number of LEDs at any speed (within reason). It does require a cog but it may be possible to add other housekeeping processes within the object. Since it's all written in Spin, it shouldn't be very hard to modify.
  • SteveWoodroughSteveWoodrough Posts: 190
    edited 2012-11-24 19:37
    Duane,
    THANK YOU. That certainly is significant. I was just looking over some experiments I had done with the GPS module a few months back and was agast at addtional references to FDS. More COGS? I thought, Oh no! Then I got a bright idea: Can I use one instance of FDS to listen to the GPS and talk to the LCD? Is that pratical?

    I will certainly download and explore Tracy's FDS object and attempt to integrate into the project.

    Best Regards
    Steve
  • Duane DegnDuane Degn Posts: 10,588
    edited 2012-11-24 19:53
    Can I use one instance of FDS to listen to the GPS and talk to the LCD? Is that pratical?

    Yes, as long as they are both using the same baud.
  • Tracy AllenTracy Allen Posts: 6,664
    edited 2012-11-25 00:04
    I've attached a couple of demo objects that show how cog clog can come about. The demo "combinedObject" starts two serial ports. The first of those sends bytes out to the system serial port at 9600 baud, and those bytes come from a second serial port set up at 2400 baud. The demo also starts a second cog, where a third serial port simply generates serial strings that it sends to the first cog. Using FDS, that scheme ends up using 5 cogs, two as spin interpreters and 3 for the pasm to support the 3 serial ports.

    The other demo does the same thing, but breaks the main program and the string generator into two separate objects, just to show how it is done. "topObject" and "childObject". Still 5 cogs. In either case there is only one copy of the spin code for fullDuplexSerial, and its methods are used by of the spin cogs, but there are three pasm cogs to support the 3 serial ports. You don't see much change in the HUB footprint as you add instances of FDS. But the cog usage is increasing rapidly.

    Duane mentioned the 4-port object that I worked on. My projects (instrumentation) tend to be serial port intensive. The 4-port pasm for 4 serial ports resides in one single cog. The startup code is a bit more complicated, and the object itself has a larger footprint, but it does make efficient use of cog power.
  • Tracy AllenTracy Allen Posts: 6,664
    edited 2012-11-25 00:12
    Another thing you may find as you get into projects with multiple objects is that you want to use the same serial port in multiple objects. Debugging is an example of that. Say you make a one object for the LCD to display ongoing guidance readings, but you still want to use the LCD for user menus that naturally fit in a different cog. FullDuplexSerial is the type of object that creates a separate instances, each with their own variables and buffers, and it is awkward to share one FDS serial port across objects. The 4-port on the other hand is written with its variables in the DATa space, and as Mike mentioned above, such objects have only one instance and it can be shared across objects. The 4-port object can be declared and used in multiple objects, but even so there is only one common HUB footprint and one pasm cog. The 4 serial ports are usually started in the top object.

    Your original question is about architecture of objects and cogs, and the difference between the uses of VAR vs DAT are one important aspect of that.
  • SteveWoodroughSteveWoodrough Posts: 190
    edited 2012-11-25 11:37
    Tracy,
    I downloaded your FDS4Port object and I am reading the notes and getting familiar with the object functionality. This is going to take me a few hours to digest to the level I need.

    A few months back when I was experimenting with the GPS module, I learned that I needed to use the "Numbers" object to translate the string data from the GPS into something useable by the program. Does your DataIO4 object perform the same function as the Numbers object?

    Thank You
    Steve
  • Tracy AllenTracy Allen Posts: 6,664
    edited 2012-11-25 19:42
    The FDS4Port object evolved from the original version developed by Tim Moore, and Tim had included various methods borrowed from other objects for numerical and string output and input. I took those out and moved them all over into the object dataIO4port.spin. That can be either re-merged with the 4-port object, or kept separate, depending on your philosophy of object inclusiveness.

    With regard to your question, you might want to take a look at Tim's original OBEX object, pcfullDuplexSerial4fc, the demo for which is a complete project called "gpstest". It does in fact include a GPS parser using those methods that are in dataIO4port

    The "Numbers" object by Jeff Martin is a marvel of condensed Spin programming. It allows conversion of numbers in any base 2--16 to a string, and vice versa, with a wide variety of formatting options. It is powerful, but hard to appreciate due to its flexibility. A great example to study. There are other "simple_numbers" options that only allow limited formatting options on decimal, hex and binary numbers. The methods in dataIO4port are of that type. Good for GPS.
Sign In or Register to comment.