Skip to content

Commit 921b890

Browse files
authored
decide on how to proceed with the direct dependency to 3rd party HAL (#107)
- #105 Currently, we are in violation of the requirement to only use libraries and frameworks through adapters, as mandated by the objectives of the software architecture. The changes propose a decision on how to resolve this violation. This will resolve #105.
2 parents ea68114 + c1cdc1f commit 921b890

4 files changed

Lines changed: 196 additions & 9 deletions

File tree

doc/decisions/dr-004.md

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# DR004 Direct Usage of Hardware Abstraction Layer
2+
3+
## Context
4+
5+
Currently, we are in violation of the requirement to only use libraries and frameworks through adapters, as mandated by the objectives of the software architecture (see
6+
[relevant section in Software Architecture](<\ref flexible_structure> "relevant section in Software Architecture")).
7+
Currently, we utilize an implementation of the [ArduinoCore-API][ACA] (ACA) as a hardware abstraction layer (HAL) from outside the adapters layer.
8+
More specifically, we employ an implementation of the ACA from Espressif, tailored for ESP32s.
9+
10+
It is generally understood that using HALs is only necessary from the
11+
[board adapters package](<\ref board_adapters> "board adapters package").
12+
13+
[ACA]: https://github.com/arduino/ArduinoCore-API
14+
15+
## Options
16+
17+
There are several options for resolving this violation of our own rules.
18+
19+
### Option 1
20+
21+
Adhere to the current guidelines.
22+
23+
According to the current state of the software architecture, we should use the 3rd party HAL indirectly through adapters.
24+
These adapters would reside in the
25+
[3rd party adapters package](<\ref third_party_adapters> "3rd party adapters package").
26+
27+
#### Option 1a)
28+
29+
For an object-oriented (OO) variant, defining an interface and inheriting from it would require:
30+
31+
- Defining one or several interfaces inside the
32+
[board adapters package](<\ref board_adapters> "board adapters package")
33+
(header files).
34+
- Implementing those as adapters inside the
35+
[3rd party adapters package](<\ref third_party_adapters> "3rd party adapters package")
36+
(source files).
37+
- Defining interfaces for the factories (see
38+
[dependency injection](<\ref dependency_injection> "dependency injection"))
39+
inside the
40+
[board adapters package](<\ref board_adapters> "board adapters package")
41+
(header files).
42+
- Implementing those factories inside the
43+
[3rd party adapters package](<\ref third_party_adapters> "3rd party adapters package")
44+
(source files).
45+
46+
This is the typical way dependency injection, and therefore the
47+
[dependency rule](<\ref interpretation_dependency_rule> "dependency rule"),
48+
is implemented.
49+
50+
#### Option 1b)
51+
52+
Instead of using an object-oriented approach using inheritance, one could create an interface to the HAL based on free functions.
53+
54+
This option would require:
55+
56+
- Defining one or several interfaces (free function declarations) inside the
57+
[board adapters package](<\ref board_adapters> "board adapters package")
58+
(header files).
59+
- Implementing those as adapters inside the
60+
[3rd party adapters package](<\ref third_party_adapters> "3rd party adapters package")
61+
(source files).
62+
- If the build configuration (see [`lib_ldf_mode`][1]) requires it, one may have to define
63+
[proxy headers](<\ref proxy_header> "proxy headers")
64+
(header files).
65+
66+
Factories are not necessary because this approach does not rely on objects.
67+
68+
[1]: https://docs.platformio.org/en/latest/projectconf/sections/env/options/library/lib_ldf_mode.html "PlatformIO documentation of lib_ldf_mode"
69+
70+
#### Option 1c)
71+
72+
A hybrid approach (OO and free functions) is also possible.
73+
This would fit well with the ACA as it also consists of classes and free functions.
74+
75+
The required steps would be a mixture of those of options 1a) and 1b).
76+
77+
### Option 2
78+
79+
Change the current guidelines so that direct dependency on 3rd party HALs is allowed within hardware-related packages (currently only the
80+
[board adapters package](<\ref board_adapters> "board adapters package")).
81+
82+
This approach eliminates the requirement to define 3rd party adapters for the HAL.
83+
84+
In order to test code depending on the ACA, the 3rd party interface needs to be stubbed.
85+
*For vanilla ACA, stubbing the proprietary interface can be greatly simplified by using [ArduinoFake][].*
86+
We utilize an [implementation of the Arduino Core API (ACA) specific for ESP32s by Espressif](https://github.com/espressif/arduino-esp32/) which extends the ACA.
87+
Following this option's approach, to directly use ACA and ArduinoFake for testing, we would need to manage the extensions separately.
88+
The following variants of this option specify different methods to handle the extensions.
89+
90+
[ArduinoFake]: https://platformio.org/lib/show/1689/ArduinoFake "ArduinoFake in PlaftormIO's registry"
91+
92+
#### Option 2a)
93+
94+
Instead of directly using a modified Arduino interface provided by the extensions (for example for a specific `class`), we use an adapter.
95+
The adapters would be integrated just as for option 1.
96+
But compared to option 1, this only needs to be done for those smallest indivisible interfaces which are not compatible with the vanilla ACA.
97+
98+
#### Option 2b)
99+
100+
At those places in the source code where extensions are used, we introduce conditionally compiled sections.
101+
One section uses the modified version of the ACA, necessary for the productive code.
102+
The alternative section is used when compiling for native tests.
103+
It uses the vanilla ACA which can be simply stubbed using ArduinoFake.
104+
105+
The selection of conditionally compiled sections is done using `constexpr if`, where possible.
106+
Else, the C preprocessor (CPP) is used for conditional inclusion (`#if`, ...).
107+
108+
## Comparison of Options
109+
110+
### Comparing variants Option 1a), b), and c)
111+
112+
Assuming that we do define interfaces for one or several HALs, we should not follow the structure of the currently used one just for convenience (as option 1c suggests).
113+
This could complicate writing adapters for different HAL implementations.
114+
Allowing variants is one of the purposes of the adapters.
115+
116+
Instead, we should design the interfaces to fit the usage.
117+
In general, an object-oriented approach is sensible to represent the different peripheral components available through HALs.
118+
Also, an object-oriented approach is the most straightforward for implementing dependency inversion.
119+
120+
From these variants, **option 1a)** would be the best.
121+
122+
### Comparing Options 1a), 2a), and 2b)
123+
124+
- **Option 1a)**: Implement and use adapters for the complete 3rd party HAL(s)
125+
- Pros:
126+
- Clean separation of concerns, dependency inversion, and injection.
127+
- Fits the overall design to use dependency inversion to fulfill the
128+
[dependency rule](<\ref interpretation_dependency_rule> "dependency rule").
129+
- Using a different implementation of our - to be defined - HAL interface would not require modifying the users of our HAL interface.
130+
Instead, only a different adapter - implementing our HAL interface - would be required.[¹](<\ref dr004_1 "¹")
131+
- Cons:
132+
- This approach requires writing adapters for all used HAL interfaces to 3rd party libraries.
133+
Designing a reusable interface is challenging, and the adapters may result in boilerplate code.
134+
Note that this approach would mean writing adapters to a HAL implementation, which is an adapter (to proprietary peripheral drivers) in itself.
135+
- **Option 2a)**: Use adapters only for extensions to vanilla ACA
136+
- Pros:
137+
- Compared to option 1a), only a fraction of the adapters would need to be written.
138+
- Cons:
139+
- Our board adapters would have a high degree of dependency on the ACA.
140+
The *Single Responsibility Principle* is violated[²](<\ref dr004_2 "²") as the board adapters would not only depend on the board and its hardware but also on an interface of a 3rd party HAL.
141+
- Dealing with the extensions made compared to vanilla ACA requires writing some adapters.
142+
- Adapting a variation from the ACA in one member function of a class requires creating an interface containing all used member functions of that class, even if those other member functions are used without alterations.
143+
This introduces *boilerplate code*.
144+
- **Option 2b)**: Use alternative sections within source files
145+
- Pros:
146+
- Reduces the need for additional code to a minimum.
147+
No adapters need to be written to use the HAL.
148+
- Cons:
149+
- Our board adapters would have a high degree of dependency on the ACA.
150+
The *Single Responsibility Principle* is violated[²](<\ref dr004_2 "²") even further (compared to option 2a)) by depending on the vanilla ACA required for testing in the native build environment only.
151+
Changes to the test infrastructure should not necessitate changes in the source file containing productive code.
152+
- In general, conditionally compiled sections of code reduces its readability.
153+
154+
\note \anchor dr004_1
155+
¹: *In practice*, our HAL interface may not be suitable to accommodate the new HAL implementation (see [Rule of Three](https://en.wikipedia.org/w/index.php%3Ftitle%3DRule_of_three_%28computer_programming%29%26oldid%3D1173684754)).
156+
Thus, we would have to adjust our HAL interface and the code using it accordingly.
157+
In other words, it is difficult to design a HAL interface such that we would actually benefit when changing the HAL implementation to a different 3rd party library.
158+
Thus, this advantage may not be relevant for that case.
159+
Also, in case the HAL implementation is changed, it may be less effort to adjust all the uses of the new HAL interface than to write adapters for the changed 3rd party library.
160+
161+
\note \anchor dr004_2
162+
²: The source file (of a non-adapter) would depend on a 3rd party interface.
163+
Changes to that 3rd party interface may necessitate changes to the source file even though the requirements to that source file remain the same.
164+
165+
### Summary & Decision
166+
167+
Writing adapters adds additional effort, potentially resulting in boilerplate code, and the advantage is dubious.
168+
169+
After careful consideration, we've decided on:
170+
171+
> **Option 2:** Allow direct dependencies from hardware-related code to 3rd party HAL interfaces.
172+
173+
And more specifically for testing on native environments, use:
174+
175+
> **Option 2b):** Where the ACA is used, it is allowed to add small conditionally compiled sections for tests to cope with deviations of the ACA used in productive code from the vanilla ACA.
176+
177+
The latter relaxation is to facilitate the use of ArduinoFake to stub the HAL's interface.
178+

doc/proxy_header.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Proxy header {#proxy_header}
2+
3+
Proxy headers serve as hint to the PlatformIO Library Dependency Finder (LDF) to find
4+
the corresponding source file (see LDF [mode][] `chain`).
5+
The actual interface is defined in the package where it is used.
6+
This is necessary for cases where the PlatformIO build environment does not use `deep`
7+
as LDF mode.
8+
Typically unit tests can not use LDF mode `deep` as this would prevent mocking source
9+
code which is not part of the unit under test.
10+
11+
This is used in case an interface is not inherited but instead forwarded to the actual
12+
implementation.
13+
14+
[mode]: https://docs.platformio.org/en/latest/librarymanager/ldf.html#dependency-finder-mode "PlatformIO Dependency Finder Modes"

doc/software-architecture.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ The objectives of the software architecture are:
1212
- A use case driven approach: The architecture shall be centered on use cases and not depend on implementation details.
1313
- Flexible structure: The software shall not be hard to adjust to changing hardware, development frameworks or external/third party libraries.
1414

15-
Achieving a Flexible Structure
15+
Achieving a Flexible Structure {#flexible_structure}
1616
------------------------------
1717

1818
\note The description will use the term '*level*' or '*policy* level'.
@@ -30,17 +30,16 @@ For maintaining flexibility the software architecture shall adhere to:
3030
For this software the implementation details (lower level) are:
3131

3232
- The board (hardware) including the processor.
33-
- The software development kit (SDK) or [framework][PIO_FRAMEWORK].
3433
- Other external/3rd party libraries.
34+
Exceptions defined below.
3535

3636
The application (higher level policies) shall not depend on those implementation details.
3737

3838
In contrast the software will commit to the following dependencies:
3939

4040
- The code may depend on a compiler which is capable to compile C++17 as defined in ISO/IEC 14882:2017.
4141
- The code may depend on an implementation of the C++ Standard Library as defined in ISO/IEC 14882:2017.
42-
43-
[PIO_FRAMEWORK]: https://github.com/platformio/platformio-docs/blob/5ae4fa7e895f5d3a04514314b1af31b37469d274/frameworks/index.rst "List of frameworks written by PlatformIO."
42+
- The *hardware related* code may depend on an implementation of the [ArduinoCore-API](https://github.com/arduino/ArduinoCore-API).
4443

4544
Implementing a Plug-in Architecture {#plugin_architecture}
4645
-----------------------------------

lib/3rd_party_adapters/README.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,4 @@ External code may be incorporated as source code or (pre-)compiled code.
1616
That is, any changes to external code shall only impact the 3rd party adapters.
1717
That includes for example if one library is exchanged by another library which servers the same purpose.
1818

19-
The software development kits (or frameworks) should not be directly used by the own code (for example the Arduino environment).
20-
Third party (external) code may depend on the SDKs.
21-
In case the own code needs to use a SDK, using a 3rd party adapter should be aimed at.
22-
The reason for this is that this simplifies moving the own code to a different SDK.
23-
19+
Which 3rd party code may be directly used without adapter is appointed by \ref flexible_structure.

0 commit comments

Comments
 (0)