a. Intro, Summary
b. Prototype
HonOBDapt translates requests for on-board automotive computer sensor data from an ISO standard OBD II format to a Honda proprietary format used in 92-95 Honda Civics.
HonOBDapt uses a general purpose microcontroller module equipped with external digital I/O ports, the Arduino Uno Rev. 3. HonOBDapt uses a Freescale MC33660 ISOLink logic level converter integrated circuit to electrically connect the OBD II signal line to the Arduino. HonOBDapt software is written in C++ and uses the SoftwareSerialWithHalfDuplex library.
HonOBDapt supports core interactive diagnostic functionality of the OBD II standard, comprised of essential sensor data including: air and coolant temperature, spark advance, fuel injector pulse width, exhaust gas oxygen content, engine speed (RPM), short and long term fuel trim, fuel system status (open or closed loop), manifold barometric pressure, atmospheric barometric pressure, throttle position, vehicle speed, etc. There is also a basic capability of translating check engine diagnostic trouble codes (DTCs). There is no support for obtaining OBD II freeze frame data, nor any analog to the concept of continuous fuel status, EVAP system or misfire monitoring.
These limitations notwithstanding this adapter serves a crucial purpose in allowing 92-95 Honda Civics to be diagnosed with modern, readily available OBD II diagnostic scanners.
A prototype was constructed with the following: several trips to the junkyard to buy a Honda on board computer (ECU) and OBD II standard connectors, lots of hot glue and some mild eyestrain from trying to solder the MC33660, which is manufactured only in a package intended for automated assembly. The prototype was tested with an ELM compatible Bluetooth OBD II scanner. The prototype was tested with the OBD II PC software packages Scantool and OBDAutoDoctor.
Sections I, II, III: C++ Software Specification
Sections IV, V, VI, VII: Electrical and Hardware specification
Section VIII: TODO
I. Control Flow
a. Asynchronous Processing Unit Design
b. Main Event Loop
c. State controller design, considerations
II. Class Reference
a. BusCoordinator
b. BusInit
c. TryReady
d. GenericBusInitAttempt
e. GenericBusInitComplete
f. CommCoordinator
g. GenericCommMessageCollector
h. GenericCommMessageProcessor
i. HondaCommCoordinator
j. HondaCommMessageCollector
k. HondaCommMessageProcessor
l. IHandler
m. IGenericHandler
o. IISOReply
p. GenericISOReply
n. GenericHandler
q. HondaHandler
r. HondaPort, ISOPort
s. HondaMap, ISOPID
III. Patterns and Principles
SubSections a, b, c, d, e, f: Object Oriented Characteristics
SubSections g, h, i, j, k, l, m: Gang of Four Patterns
a. Polymorphism
b. Loose Coupling,
c. Separation of Concerns
d. Information Hiding and Encapsulation
e. Reuse and Inheritance
f. Generalization
g. Template
h. Command Pattern
i. Chain of Responsibility, Responsibility Driven Design
j. State, Automaton, finite state machine
k. Strategy
l. Mediator
m. Adapter
IV. Serial Ports
V. MC33660
VI. Arduino
VII. OBD II
VIII. TODO
CONTROL FLOW
Asynchronous processing unit design
Generally, classes involved in handshaking and serial communications support Work(), DoneWorking() and Reset().
These functions serve as a basic cooperative multitasking system. Work() is called when a higher level controller has a time slice to give to the unit. The Work() function of each controller is expected to do its' work, and exit quickly. If the conditions are met that allow the handshaking or communications to proceed to the next higher level, DoneWorking() should return true. Reset() is used to reset the internal state of the unit, typically at the end of a logical session controlled by a higher level controller.
Generally, the units are one-shot triggers. If, for example, a unit is falsely triggered to initiate a particular process ( by noise or an invalid packet ), it will continue to attempt completion of the process and cannot properly proceed with a valid sequence until it is Reset() by a higher level controller.
Main Event Loop
The main event loop cycles through the upper lever coordinators, BusCoordinator, CommCoordinator and HondaCommCoordinator in a round robin fashion, calling .Work() and checking .DoneWorking() on each one.
The OBD II port requires a bus initialization handshaking sequences. No bus initialization is necessary on the HondaPort.
The main event loop strives to move the system into higher and higher levels of functioning. The macro levels of functioning are:
Level 1. OBD II bus not initialized
Level 2. OBD II bus initialized, waiting for incoming request from the OBD II port.
Level 3. Message received from the OBD II port and forwarded to the HondaPort, waiting for message from HondaPort
Level 4. Message received from the HondaPort and forwarded to the OBD II port.
Once the functioning level reaches 4, the process restarts generally at level 2, but possibly level 1.
Level 1.
The main event loop maintains a state variable that is either NO_COMM or COMM_ESTAB. Each time through the main event loop, if the state is NO_COMM (bus not initialized), then BusCoordinator.Work() is called.
The BusCoordinator will work to initialize the OBD II bus and achieve a higher state, COMM_ESTAB, by using TryReady, BusInit, GenericBusInitAttempt and GenericBusInitComplete. After the call to BusCoordinator.Work(), BusCoordinator.DoneWorking() is checked. If this function returns true, then the bus is ready for communications and the state is set to COMM_ESTAB. if DoneWorking() is not true, the main event loop restarts, again attempting to raise the functioning level by successfully initializating the OBD II bus.
Level 2.
Once the bus is initialized, CommCoordinator.Work() will be called to try to continue to raise the processing level.
CommCoordinator will use GenericCommMessageCollector to get any incoming data on the OBD II port.
If there is no data available immediately on the serial port, the GenericCommMessageCollector.Work() will exit immediately; this allows the CommCoordinator to exit and yield control to the main event loop.
If there is waiting data, the data will be retrieved and a state of COLLECT will be marked internally to GenericCommMessageCollector.
During the next iteration through the main event loop, CommCoordinator.Work() will be called and the Work() function will call GenericCommMessageCollector.Work(). If there is more data available on the OBD II port, that data will be collected by the GenericCommMessageCollector. Since the internal state of GenericCommMessageCollector is COLLECT, the new data will be appended to the existing message buffer.
Level 3
A true return value from GenericCommMessageCollector.DoneWorking() signifies that a complete and valid OBD II message has been received and the internal state of the coordinator, CommCoordinator, will be marked as MESSAGE_RECEIVED. Then the GenericCommMessageProcessor will become active, processing the request by calling HondaHandler.IncomingPID(). The HondaHandler will translate the OBD II PID to an appropriate request for Honda data and will call SendHondaRequest() to send a command sequence to the HondaPort. HondaHandler will then assign the request to the HondaCommCoordinator, which queues the request, using a function pointer to establish a non-blocking asynchronous callback to be executed when the HondaPort data is ready.
CommMessageProcessor and CommCoordinator then yield control to the main event loop.
During the next iteration of the main event loop, HondaCommCoordinator.Work() will be called and the HondaCommCoordinator will pick up the queued request and set an internal state of WORKING. The HondaCommCoordinator will use the HondaCommMessageCollector to get any incoming data on the HondaPort. If there is waiting data, the character data will be retrieved and a state of COLLECT will be marked internally in the HondaCommMessageCollector
DUring the next iteration through the main event loop, HondaCommCoordinator.Work() will be called. If there is more data, that data will be collected by the HondaCommMessageCollector. Since the internal state of HondaCommMessageCollector is COLLECT the new data will be appended to the existing message buffer.
Level 4
When the message from the HondaPort is complete and valid, signaled by the HondaCommMessageCollector returning true to DoneWorking(), the HondaCommMessageProcessor will become active and process the message by executing HondaHandler.Callback(). HondaHandler.Callback() will execute the appropriate return function matching the original asynchronous request and effect the translation of the Honda sensor data into ISO measurements, using a member of the group ISOMeasurementsTOISOReply, and the reply to the OBD II port, using a GenericHandler.IISOReply family member.
At this point the overall state has reached its highest point of productivity. The various MessageCollector(s) and MessageProcessor(s) are reset, and the main event loop restarts at level 2, waiting for more serial messages from the OBD II port.
-- Advantages of state controller design
---Extensibility
This structure provides a clean, clear implementation that lends itself to further modifications and extension, such as hooks to process other data or timeout watchdogs, because there are clear places to hook into the overall flow of handshaking or messaging without the risk of undesirable interaction or needing to understand the lower level operation of each controller.
This structure also provides a clean way to implement potentially deep nested logic in a clear, easy to debug and trace fashion.
Each asynchronous unit is coded with the overall structure in mind. For example, TryReady.Work() will not block but will do a small amount of work, polling the OBD II K-Line bus status and checking the time-of-day ( millis() ) to maintain a running total of idle time and then Work() will exit. TryReady can maintain internal state such as the running total but encapsulates that state and signifies that internal conditions have been satisfied by returning true to DoneWorking(). Also, when logical conditions require, Reset() can be used to reset the running total.
In this way serial communications or handshaking can be carefully controlled from the upper level.
For instance, this structure makes it easy and clear to implement an OVERALL handshake attempt timeout, without modifying functionally distinct units such as TryReady.
---Error Handling
When dealing with serial port messaging, subject to noise, message fragments being received, etc,
it is quite possible for individual functional units to become stuck or hung due to: waiting for message termination or validation that will never succeed because of mid-message corruption, invalid or nonstandard messages, etc.
In this type of messaging environment it is important to maintain supervisory control with clear top-down control logic. The asynchronous, non-blocking Work(), DoneWorking() pattern lends itself to implementing supervisory controls.
Of paramount importance is that the supervisory code and its' interface to lower level code be clear, distinct and concise, so that unforseen situations; i.e. dealing with slightly altered or new packets or protocol details and their concomitant issues; can be dealt with effectively.
---Complexity and correctness
The factoring of complicated nested communications logic into a body of state machines is an effective way of managing complexity and makes logical defects more apparent. This factoring methodology allows the program to be treated as the steps of an algorithm, facilitating the application of mathematically rigorous techniques to prove or disprove the correctness of the program logic.
-- Possible ramifications of state controller design
Although the event loop design of the serial communications code has additional overhead compared to a more direct low-level approach, and can cause an entire event loop to be executed between individual serial character data retrievals, the overhead is mostly syntactic exposition, and is not expected to be computationally intensive.
As well, 9600 baud is not particularly fast relative to Arduino's microprocessor speed and the SoftwareSerial library is interrupt driven ( interrupts take priority over all other execution context ) and SoftwareSerial maintains its' own input buffers. Also, the direct serial controllers, CommMessageCollector and HondaCommMessageCollector are enhanced with a burst mode that increases their priority under certain circumstances. Due to these factors it is considered extremely unlikely for the overhead of the state machine control system to cause a loss of serial data on any port.
CLASS REFERENCE
BusCoordinator
The BusCoordinator is responsible for coordinating the bus initialization process including: bus idle time prerequisite, wakeup-packet handshaking and confirmation handshaking.
The Buscoordinator uses TryReady, BusInit, GenericBusInitAttempt, and GenericBusInitComplete.
TryReady
TryReady polls the OBD II K-Line bus pin and establishes that the necessary bus idle condition has been met before handshaking begins.
BusInit
Tasked with the job of attempting and completing an OBD II bus initialization handshake sequence. Uses GenericBusInitAttempt and GenericBusInitComplete.
GenericBusInitAttempt - establishes generic requirements for a bus initialization attempt, which begins the handshaking sequence of an OBD II session. Uses WAKE_PACK array, a storage area for K-Line bus pin logic level transition times ( 10ms, 100ms, etc ). This routine uses Arduino's digitalRead to check the logic level of the OBD II K-Line bus pin. When the pin alters state ( High to Low or Low to High ) the time-of-day ( millis() ) is recorded. This recorded value is compared to the time-of-day when the next level transition occurs, and in this way the length of the handshaking pulse or pulses is established.
If the transition pattern is a valid OBD II K-Line bus wake-up pattern, DoneWorking() will return true.
The protocol specific details of the GenericBusInitAttempt are left to the more derived classes
BusInitAttempt_ISO14230_slow, BusInitAttempt_ISO14230_fast, BusInitAttempt_ISO9141
GenericBusInitComplete
establishes basic requirements of bus initialization completion which finishes the handshaking sequences for an OBD II session. The completion of the handshaking is accomplished by sending a confirmation message, mathematically derived from the messages received during a bus initialization attempt, to the OBD II port.
The protocol specific details of the BusInitComplete are left to the more derived classes:
BusInitComplete_ISO9141, BusInitComplete_14230_slow, BusInitComplete_ISO14230_fast
CommCoordinator
The CommCoordinator is responsible for collecting, verifying and processing messages on the OBD II K-line serial port. Processing generally concludes with a call to HondaHandler.IncomingPID(), which accepts responsibility for replying to the OBD II K-line serial port with the requested Honda on board computer (ECU) data. Uses GenericCommMessageCollector and GenericCommMessageProcessor.
GenericCommMessageCollector
Collects the serial data from the OBD II port and establishes completeness (appropriate headers) and basic error-checking (checksum) of incoming serial data.
This message collector supports a serial burst mode, assuming that an incoming serial character is usually followed by the remaining characters without delay. This routine is authorized to busy-wait (block) waiting for serial data for a maximum time interval defined by SERIAL_BURST_MAX_TIME_SLICE, before relinquishing control to its' coordinator.
Protocol specific details are left to the more derived classes: ISOCommMessageCollector_ISO9141, ISOCommMessageCollector_ISO14230
GenericCommMessageProcessor -
Processes a valid message containing an OBD II PID by calling HondaHandler.IncomingPID() to transfer responsibility for retrieving the requested data and sending a reply to the OBD II port. IncomingPID function does not block and GenericCommMessageProcessor and its' coordinator CommCoordinator can exit immediately after the call to IncomingPID.
Protocol specific details are left to the more derived classes: CommMessageProcessor_ISO9141, CommMessageProcessor_ISO14230
HondaCommCoordinator manages Honda communications. Uses HondaCommMessageCollector and HondaCommMessageProcessor.
Works in conjunction with the HondaHandler. Provides AssignWork function which, when called by HondaHandler, queues a request for Honda data.
HondaCommMessageCollector
Collects the serial data from the HondaPort. Establishes completeness, appropriate headers, error-checking (checksum) of incoming serial data.
This message collector supports a serial burst mode, assuming that an incoming serial character is usually followed by the remaining characters without delay. This routine is authorized to busy-wait (block) waiting for serial data for a maximum time interval defined by SERIAL_BURST_MAX_TIME_SLICE, before relinquishing control to its' coordinator.
HondaCommMessageProcessor
Processes valid messages from the HondaPort. Processing concludes with a call to HondaHandler.Callback(), which effects translation of the Honda data and the reply to the OBD II port using a member of the GenericHandler ISOMeasurementsToISOReply group and an IISOReply family member.
IHandler establishes what a handler, used by the CommCoordinator.CommMessageProcessor, must do
A class implementing IHandler must support IncomingPID. At a fundamental level, to implement the IHandler interface the class must be able to do something with IncomingPID, perhaps ignoring unsupported operations. This class would be a good jumping off point for diverse purposes, for example, a class that only logs the incoming messages.
IGenericHandler supports VIN and Capabilities.
A slightly richer implementation would be required to, at a minimum, reply with the Vehicle Identification Number and the Capabilities, which is a list of supported sensors or other data that can be retrieved.
IISOReply is an interface defining the generic requirements of a reply to the OBD II port.
GenericISOReply is class containing generic OBD II port reply code.
The protocol specific details are left to the more derived classes: ProtocolReply_ISO9141 and ProtocolReply_ISO14230.
GenericHandler
Adds an interface of virtual function prototypes that describe the expected input and output for a sensor data retrieval implementation and a baseline map function, ISOPID_TO_GENERIC_HANDLER_MAP, that links OBD II PIDs to functions in the sensor interface. GenericHandler builds directly upon its direct ancestor abstractions IGenericHandler and IHandler, but becomes richer and more concrete by extending outside of that family to become a composition with an IISOReply family member.
GenericHandler also defines functions that convert data to an OBD II format. These functions follow the naming convention ISOMeasurementsToISOReply..XXXXX. These functions accept input data in the scale dictated by the OBD II standard; i.e. temperature is measured in Celcius degress, with the input data to the function represented by the formula "Actual Temp - 40". E.G., if the temperature of the coolant is determined to be 55 deg. C., you would call ISOMeasureToISOReply_Scalar_byte( 15 );
The inclusion of an IISOReply family member which includes functions that reply to the OBD II port and the definitions of the ISOMeasurementsToISOReply functions establishes that at this level of class abstraction that the concept of handling will be to send a reply message to the OBD II port. The IISOReply member also provides functions toward this end. At this level, the implementation of the sensor data virtual functions is left abstract.
HondaHandler becomes the most derived, richest member of this family tree. It is a complete, fully functional handler. It adds concrete implementations of the remaining abstractions in the GenericHandler family hierarchy, the abstract sensor data retrieval interface and IncomingPID().
The HondaHandler implements IncomingPID by using the generic mapping of OBD II PIDS onto the abstract sensor data retrieval interface, ISOPID_TO_GENERIC_HANDLER_MAP. The HondaHandler implementation of the sensor data retrieval functions is asynchronous, and delegated, relying on the placement of its' functional colleague, HondaCommCoordinator, in the main event loop, to retrieve the data from the HondaPort.
HondaCommCoordinator.AssignWork() is used to communicate with the HondaCommCoordinator, requesting that during the next main event loop cycle, the retrieval of a particular sensor value from the Honda on board computer (ECU).
AssignWork establishes a callback function pointer for the HondaCommCoordinator to call when the requested data has been retrieved.
When the HondaCommCoordinator responds with the data, HondaHandler converts, maps or otherwise translates the data values from the Honda scaling or other representations, into values compatible with the OBD II measurement scale or representation. Then a member of the GenericHandler.ISOMeasurementsTOISOReply...XXXX group is called to generically convert the data into an OBD II format and then use an IISOReply family member to effect the reply to the OBD II port using the proper protocol.
HondaHandler implements the IGeneralAsyncHandler interface, marking it as an asynchronous class.
HondaHandler defines GeneralAsyncRequest, a function that factors out the common code for setting up the function pointer.
IGenericAsyncHandler
IGenericAsyncHandler is a marker interface. Implementing it marks a class as asynchronous in nature.
ISOPort, HondaPort
ISOPort and HondaPort provide character based access to the serial ports, defined using a SoftwareSerial instance configured to communicate on the digital I/O pins connected to the OBD II connector and the Honda Diagnostic Link Connector.
ISOPort and HondaPort provide safety features by providing a consistent interface to the serial ports, which can often become unmanageable within nested serial communications logic.
All reads and writes are done through these interfaces, so the checksums are calculated through these routines, reducing the risk of multiple conflicting direct access.
HondaMap
HondaMap objects are used to refer to specific sensors in the Honda on-board computer (ECU). The definition includes an address and length. Some sensors return two bytes, for example, RPM. If the two-byte return data is a scalar value, the data is big-endian, that is, the most significant byte, the one that counts for 256 for each 1 of its' value, is the first of the two-bytes transmitted by the ECU.
ISOPID
ISOPID objects are used to represent OBD II requests. Each request contains a mode and an identifier. Mode 1 is sensors. Mode 3 is Diagnostic Trouble Codes. E.G. Mode 1, identifier 0x0C is engine speed (RPM).
PATTERNS
Object Oriented characteristics:
Polymorphism
GenericBusInitAttempt, ISO14230_BusInitAttempt, ISO9141_BusInitAttempt
When the variable oBusInitAttempt takes the form of a ISO14230_BusInitAttempt, the BusCoordinator doesn't know or care about this. All the BusCoordinator needs to know is that a GenericBusInitAttempt is being utilized. Thus the principle of polymorphism, or many forms, is being leveraged to achieve more generalized code. From one perspective the form of the instance variable oBusInitAttempt is GenericBusInitAttempt and from another perspective the form is ISO14230_BusInitAttempt, thus polymorphic.
IHandler, GenericHandler, HondaHandler
The IHandler abstract interface is used in the CommMessageProcessor. The form of the instance variable in CommMessageProcessor is an abstract IHandler at compile time. At run time, however the IHandler object takes the form of a concrete HondaHandler. From the perspective of the CommMessageProcessor the instance variable is an IHandler, but from a macro, system level view, the instance variable is a HondaHandler. Thus this variable takes many forms and is polymorphic. GenericHandler is another form of an IHandler, albeit a kind of abstract 'unexpressed' form, as instantiating it would result in an incomplete handler implementation.
Loose Coupling
Even though they work closely together to accomplish the overall goal in this adapter implementation,
there is a loose-coupling between the CommMessageProcessor, receiving a PID from the OBDII port and the HondaHandler, effecting a reply to the OBD II port. This coupling is loose because the Handler could be implemented in different ways and this loose coupling leaves the door open for more diverse interpretations of the term 'handling'.
In this adapter implementation the loose coupling make it easier to inject an asynchronous handler into the design. The CommMessageProcessor is coupled only to an IHandler, an interface definition ( aka, contract specification ), not tied directly to the asynchronous implementation of HondaHandler, reducing the chance of cascading changes rippling through multiple functional units.
One of the advantages of the loose coupling in this adapter implementation is its' ability to mitigate the situation where one subsystem is blocked by another. It would be undesirable to make the CommMessageProcessor block(wait) for the Honda data to be retrieved. The data may never come but we still want the CommMessageProcessor to be able to degrade gracefully and respond to its' immediate supervisor. The loose coupling supports this because the HondaHandler doesn't take control when it is tasked with getting Honda sensor data. It is delegated to do so and this is a pattern of Chain of Responsibility, but the CommMessageProcessor maintains control and can exit cleanly. This clean exit facilitates the placement of supervisory control logic where it belongs; at a high logical level in the code, not buried deep in the implementation details of a particular low level unit.
Loose coupling has its benefits and downfalls. Loose coupling allows flexibility in implementing the Handler without changing the CommMessageProcessor, and ultimately aids in the implementation of an asynchronous handler, but this does not come without issues. For example, there is no way to easily enforce transactional integrity across this coupling, or pass error or control messages such as 'retry' across this coupling because by the time a failure is detected on one side of the coupling, the execution context that initiated the request has exited. A similar problem occurs with non-asynchronous loose coupling, where, for example, it is impossible to pass an error message because the loose coupling only supports a limited interface.
Separation of Concerns
See Patterns and Principles "Loose Coupling", "Information Hiding and Encapsulation", and "Chain of Responsibility"
Generalization
-- Reuse, Inheritance and Templates
--- Benefits
The sharing of common functionality reduces the overall code load that must be inspected. It also increase the chances that changes made to the system will propagate in the proper way.
--- Ramifications
The factoring out of common functionality must be done thoughtfully and carefully, as the benefits of reuse are quickly outweighed by the complexity of managing entanglement between sharing classes.
See Also, Generalization, composition, flexibility of modularity
see Also, Generalization, composition, ramifications
See Also, Generalization, overgeneralization
See Also, Template for examples; Generally whereever there is a Template there is reuse.
Generalization, composition
-- Reuse, factoring, flexibility of modularity
--- Benefits
Factoring out distinct conceptual groups of functionality like sensor data virtual function interface, ISOMeasurementsTOISOReply grouping, IISOReply and GenericISOReply provides the flexibility of modularity. Individual pieces can be swapped in and out, e.g. substituting a newer Controller Area Network diagnostic subsystem implementation for the OBD II system, as long as the new piece adheres to the established convention, defined by interfaces.
Flexibilty means less risk. Generalizing may take more front-end design time, but reduces the amplitude of wild-schedule swings on the back end.
--- Ramifications
Compositional reuse can lead a design down a dead end. If a class inherits everything from a composed class and implements a new subsystem, without a solid set of abstract interfaces ( or contracts ) to define exactly where in the tree the new functionality branches off from ( or differs from the previous incarnation ), it is likely that new functionality will simply rewrite existing functionality.
So there is a place on the spectrum between strict inheritance hierarchy-to-god class compositional multipurposing that must be chosen carefully. Too tight interface adherence limits the diversity that future implementations can create. Too little interface adherance and excessive use of ad-hoc compositional reuse encourages rewrites and big-ball-of-mud designs.
Overgeneralization
The attempt to use generic structures is a noble goal, but there is a limit to how far to take it. The architect of class families must focus on the real purpose of the design effort. Is it to solve a particular problem, with a flexible building block pattern, that reduces the risk that changing requirements will render the code unusable?
Or is the design effort moving dangerously toward overgeneralization. For example, a good home builder might carry a variety of tools, saws, hammer, etc, some of which may even be used to build other tools. e.g. scaffolding for painting. But the home builder doesn't carry a smelting forge in the truck. He doesn't need that much generality and to attempt such would actually be counterproductive - for building a house - so the principle is to keep some perspective about the goal you are trying to achieve.
Too much factoring and generalizing can cause unnecessary complexity. As the class family becomes more fragmented and modular it becomes harder for the consumer to understand how to use it.
So the place on the spectrum of generalism-to-specialization is important. There is always a design trade-off. As Spolsky says, the trash can must be open on top so people can put trash in. But it needs to be closed on top so trash doesn't blow away.
Generalization, practical implications and best practices
The structure of the IHandler family, beginning with the IHandler interface and ending with the HondaHandler.
is constructed with inheritance and composition.
The HondaHandler inherits functionality from the trunk, GenericHandler. But GenericHandler is actually formed of inheritance from IGenericHandler and IHandler, but also of composition with another family tree, IISOReply. So the trunk HondaHandler inherits is already a compositional mixture and not a strict inheritance hierarchy of 'is a' relationships. The mixing of the ISOMeasurementsTOISOReply functions and an IISOReply family member into the IHandler family hierarchy effectively constrains handlers derived from GenericHandler to being OBD II-based handlers.
HondaHandler is also formed with a mix of inheritance and composition; inheriting from GenericHandler but also formed by being composed with IGenericAsyncHandler.
In principle, if the classes form a definite tight hierarchy and really could not be split into separate pieces that are independently functional, use inheritance. Another rule of thumb is if the relationship is a direct building upon , use inheritance. If the relationship is a kind of association with, or equal level partnership, use composition. If the classes could be split, use a composition approach to achieve the right mix of 'lateral' building blocks.
The GenericHandler is made concrete on the OBD II side and left abstract on the Honda side because this is the most likely usage pattern. The next logical step for the IHandler family tree would be to branch off at the GenericHandler level to create a concrete implementation of another proprietary standard that needed to adapt to the now ubiquitious OBD II standard. This is a likely re-use pattern, but not the only one afforded by the generalization topology described here. It is hard to conceptualize exactly what could be constructed with the existing modules and interfaces ( or contracts ) but that is the beauty of infrastructure in the designer's opinion.
Generalization principles serve as design heuristics when building re-usable infrastructure ( or tooling ) to build Mediators like GenericHandler where one or more sides may be unknown at the time of construction of the tooling.
Mixins
The IHandler family may benefit from a more formal analysis and re-design using mixins, which are a combination of methods from separate classes.
Traits
The IHandler family may benefit from a more formal analysis and re-design using traits, a set of methods that implement a behaviour and a set of methods that parameterize that behaviour.
See Also, Class References "GenericHandler", "HondaHandler"
Template:
IHandler provides a template for any class that may be used as a handler, by CommMessageProcessor.
HondaHandler is the most derived class from this template.
IISOReply provides a template for any class that replies to the ISOPort. ISOReply_9141 and ISOReply14320 build on this template.
GenericBusInitAttempt provides a template for classes that attempt OBD II protocol bus initialization. Since there are three similar but nontheless unique possible methods of initialization (ISO9141, ISO14230_slow, and ISO14230_fast) the shared code between these three is factored into the template and only the unique characteristics remain in the most derived classes (BusInitAttempt_ISO9141, BusInitAttempt_ISO14230_slow, BusInitAttempt_ISO14230_fast).
Information Hiding and Encapsulation
ISOPort and HondaPort encapsulate the checksum running total variable, improving code safety.
Each coordinator (BusCoordinator, CommCoordinator, HondaCommCoordinator) or controller (TryReady, BusInitAttempt, CommMessageCollector, etc) hides its internal state, providing a standard interface. This improves safety and reduces the need to understand the internal details to make changes to the logic using these functional units.
HondaMap and ISOPID classes hide the low-level details of these data structures, providing a safer ( e.g. a Honda address can never be inadvertently associated with wrong length because they are stored together ), more convenient way of manipulating these logical units.
Command Pattern
GenericHandler contains a function ISOPID_TO_GENERIC_HANDLER_MAP(), that is built in a command pattern that lends itself to further ad-hoc modification by adding executable code within the respective command hooks.
Each ISOPID is treated as a command object, directing execution into the appropriate sensor data retrieval function. This pattern is extensible, for example, easily allowing for two or more separate retrieval steps or other ad-hoc functionality in the commanded subroutine.
Chain of Responsibility:
The functional colleagues CommMessageProcessor and HondaHandler exhibit characteristics of a Chain of Responsibility pattern. The CommMessageProcessor does not directly handle the gathering of the data that is required for the successful conversions of the PID it receives into Honda data. Rather, it relies on an indirect Chain of Responsibility. When a PID is received by CommMessageProcessor, a delegating call to IncomingPID is made. This shifts responsibility of actually completing this task onto another subsystem.
HondaHandler, AssignWork, GeneralAsyncRequest
The HondaHandler sends a message to HondaCommCoordinator using AssignWork to retrieve the appropriate data from the HondaPort. The AssignWork function shifts the responsibility to the HondaCommCoordinator to retrieve the data. The HondaCommCoordinator is given the responsibility for retrieving the data, and is also given the tools, in the form of a Callback function pointer, to ASYNCHRONOUSLY satisfy it's obligation of returning the data.
The BusCoordinator, BusInit and GenericBusInitAttempt (or GenericBusInitComplete) form a Chain of Responsibility pattern. The BusCoordinator is responsible for the overall bus initialization sequences, but it delegates responsibilty for this task to two associated classes, TryReady and BusInit. BusInit then transfers responsibility to its' delegates GenericBusInitAttempt and GenericBusInitComplete. Thus the responsibility for certain actions is transferred in a chain.
The main event loop and select parts of the class design could be considered an example of Responsibility Driven Design, using a Clustered/Delegated control Structure.
State:
Many examples:
The main event loop stores state about the overall OBD II connection state with two macro level states, NO_COMM and COMM_ESTAB.
BusCoordinator stores state about the state of bus handshaking.
BusInitAttempt stores state about the state of the bus initialization attempt.
Each of these units is called repeatedly to accomplish a single logical task and they keep track of where they are by a State pattern; they are guided and alter their behaviour based on their transient state.
DoneWorking() and Reset() provide consistent, safe, encapsulated access to the state.
The main event loop and all the subsidiary loops contained within could all be considered an example of one complex Automaton. These structures could also be described as finite state machines.
Strategy:
The implementation of IHandler.IncomingPID() by HondaHandler could be considered one Strategy for dealing with the incoming OBD II request. This larger unit of work seems to qualify as a strategy and diverges from the Template definition of mere alteration of subtle details by a class deriving from a Template.
Compare Template.
Mediator:
GenericHandler is a mediator because it is the only class that knows the internal details of two systems: the OBD II system and the Honda system.
From the perspective of the CommMessageProcessor, the IHandler base interface presents a simplified interface to the Honda side that consists of only one function call, IncomingPID.
From the perspective of the Honda side GenericHandler presents a simplified interface to the OBD II side that consists of a group of ISOMeasurementsToISOReply...XXXX reply functions. The input and operation of these functions is well defined and simple.
Thus the GenericHandler serves as a mediator, providing a simplified interface to a complex task of obtaining Honda sensor data as well a providing a simplified interface to a complex task of sending data to the OBD II port using the proper protocol, mediating these two sides of the system.
Adapter:
At a macro level, the entire hardware / software system is an Adapter, converting and translating the requests and data from one system into a format compatible with another system.
SERIAL PORTS
Software Serial
The SoftwareSerial library was chosen for two reasons. The Arduino only has one hardware serial port, and the best use for this port is as a debugging aid to see the trace messages from the monitor.
The other reason is that there is a need to operate the serial port at a non-standard Honda baud rate. The serial timing in SoftwareSerial.h was adjusted, though minimally, for 9470 baud. The original timing for 9600 baud worked before these adjustments were made. This baud rate is relatively low and the difference between the Honda rate and the standard rate is small, so it is possible these timing adjustments are superfluous.
Due to the nature of the SoftwareSerial interrupt driven design, only one serial port can be active at any time. Therefore, at precise junctions in the control structure, each port is activated with a .listen() statement.
Half Duplex. This section is unconfirmed and based on the best knowledge of the designer.
SoftwareSerialWithHalfDuplex is used because the communication line on the Honda Diagnostic Link Connector is half duplex, that is, only one member, the client (HonOBDapt) or the server (on board computer, ECU) can transmit data at one time. SoftwareSerialWithHalfDuplex supports this configuration by specifying the same pin for Transmit and Receive. It is the responibillty of the HondaCommCoordinator to enforce this constraint, as the behaviour of SoftwareSerialWithHalfDuplex if an attempt is made to transmit and receive at the same time is unknown.
The OBD II ISOPort is also half duplex, but is connected to the MC33660 which has a full duplex style of connection with both Receive and Transmit pin connections. This provides a physical, electrical way of enforcing half duplex on the OBD II K-Line, as a communications participant attempting to bring the line low or high while the other participant is still transmitting will simply be ignored by the circuitry in the MC33660.
MC33660
MC33660 is a level conversion integrated circuit that converts 5 volt logic signals to 12 volt logic signals. This is necessary because the main hardware component, the Arduino, uses 5 volt signals on its' digital I/O pins and OBD II specifies 12 volt signals. MC33660 is an integrated circuit specifically designed for logic level conversion in an OBD II automotive application and should therefore be particularly suited to this application in terms of susceptibility to noise, overload protection, etc. MC33660 is designed to be connected to the K-Line bus and therefore provides an appropriate electrical interface for serial communications and bus-style level transitions.
Arduino
Arduino provides 5 volt logic level digital I/O pins that are suited to this application. The application of a software library that allows serial communications on any pin provides significant flexibility. Arduino has flexible power supply requirements and can be operated on a 12 volt system. Nominal current draw through the onboard voltage regulator in this application is 200mA. Heat created by the voltage regulator dropping 12 volts to 5 volts to power the on board processor is dissipated by a small on-board heatsink, so the current limits and supply voltage combination must be carefully selected. The current configuration is within limits to the best knowledge of the designer and has been anecdotally confirmed by feeling for excess heat from the voltage regulator.
OBD II
The OBD II electrical and logical communications standard is actually a conglomeration of many existing standards at the time of creation of the OBD II 'standard' by ISO. This adapter supports the K-Line standard(s) of ISO-9141, ISO-14230_slow, and ISO-14230_fast. Only one is technically required as any OBD II compliant scanner must be able to work with all three, but the implementations vary only minimally and provide a good exercise and example of certain software design techniques.
TODO
----Asynchronous Multitasking:
There should be an ITask interface to constrain and define the exact nature of the controller(s) that have the Work(), DoneWorking(), and Reset() functions.
-----Handler class family construction:
The sensor data virtual function prototypes in GenericHandler should be split into a separate interface definition, then implemented virtually by GenericHandler and concretely by more derived classes like HondaHandler.
The ISOMeasurementToISOReply...XXXX family of functions should be extracted out of GenericHandler and put in a separate mix-in.
The IGeneralAsyncHander should be expanded and more clearly defined to include the GeneralAsyncRequest function defined in an ad-hoc manner in HondaHandler.
----Other
The HondaCommCoordinator should implement an IWorkQueue interface, clarifying the role of the Assignwork function.
The magic number(s) of the digital I/O pins used for serial communications in SoftwareSerial should be extracted out into a #define.