-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmodule.h
More file actions
604 lines (544 loc) · 29.7 KB
/
module.h
File metadata and controls
604 lines (544 loc) · 29.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
/**
* @file
* @brief Provides the Module class that exposes the API for integrating user-defined custom hardware
* modules with other library components and the interface running on the host-computer (PC).
*
* This class defines the API interface used by Kernel and Communication classes to interact with any custom hardware
* module instance that inherits from the base Module class. Additionally, the class provides the utility functions
* for routine tasks, such as changing pin states, that support the concurrent (non-blocking) runtime of multiple
* module-derived instances.
*
* @attention Every custom hardware module class should inherit from the base Module class defined in this file and
* override the pure virtual methods of the parent class with instance-specific implementations.
*/
#ifndef AXMC_MODULE_H
#define AXMC_MODULE_H
#include <Arduino.h>
#include <digitalWriteFast.h>
#include <elapsedMillis.h>
#include "communication.h"
/**
* @brief Provides the API used by other library components to integrate any custom hardware module class with
* the interface running on the companion host-computer (PC).
*
* Any class that inherits from this base class gains the API used by the Kernel and Communication classes to enable
* bidirectionally interfacing with the module via the interface running on the companion host-computer (PC)
*
* @warning Every custom module class @b has to inherit from this base class. Follow this instantiation order when
* writing the main .cpp / .ino file for the controller: Communication → Module(s) → Kernel. See the /examples folder
* for details.
*
* @note Use the utility methods inherited from the base Module class and stage-based command design pattern to ensure
* that the custom module implementation is compatible with non-blocking runtime mode. See the ReadMe for more
* information about non-blocking runtime support.
*/
class Module
{
public:
/**
* @brief Stores the data that supports executing module-addressed commands sent from the PC interface.
*
* @warning End users should not modify any elements of this structure directly. This structure is modified by
* the Kernel and certain utility methods inherited from the base Module class.
*/
struct ExecutionControlParameters
{
uint8_t command = 0; ///< Currently executed (in-progress) command.
uint8_t stage = 0; ///< The stage of the currently executed command.
bool noblock = false; ///< Determines whether the currently executed command is blocking.
uint8_t next_command = 0; ///< Stores the next command to be executed.
bool next_noblock = false; ///< Determines whether the next command is non-blocking.
bool new_command = false; ///< Determines whether next_command is a new or recurrent command.
bool run_recurrently = false; ///< Determines whether next_command is recurrent (cyclic).
uint32_t recurrent_delay = 0; ///< The delay, in microseconds, between command repetitions.
elapsedMicros recurrent_timer; ///< Measures recurrent command activation delays.
elapsedMicros delay_timer; ///< Measures delays between command stages.
};
/**
* @brief Defines the status codes used to communicate the states and errors encountered during the shared API
* method runtimes.
*
* @note To support consistent status code reporting, this enumeration reserves values 0 through 50. All custom
* status codes should use values 51 through 250. This prevents the status codes derived from this enumeration
* from clashing with custom status codes. Additionally, all custom event codes defined by each Module-derived
* class must be unique within that class to ensure unambiguous event identification by the PC.
*
* @attention This enumeration only covers status codes used by non-virtual methods inherited from the base
* Module class. These status codes are considered 'system-reserved' and are handled implicitly by the
* PC-side companion library.
*/
enum class kCoreStatusCodes : uint8_t
{
kStandby = 0, ///< The code used to initialize the module_status variable.
kTransmissionError = 1, ///< Encountered an error when sending data to the PC.
kCommandCompleted = 2, ///< The last active command has been completed and removed from the queue.
kCommandNotRecognized = 3, ///< The RunActiveCommand() method did not recognize the requested command.
};
/**
* @brief Initializes all shared assets used to integrate the module with the rest of the library components.
*
* @warning This initializer must be called as part of the custom module's initialization sequence for each
* module that subclasses this base class.
*
* @param module_type The code that identifies the type (family) of the module. All instances of the same
* custom module class should share this ID code.
* @param module_id The code that identifies the specific module instance. This code must be unique for
* each instance of the same module family (class) used as part of the same runtime.
* @param communication The shared Communication instance used to bidirectionally communicate with the PC
* during runtime.
*/
Module(const uint8_t module_type, const uint8_t module_id, Communication& communication) :
_module_type(module_type), _module_id(module_id), _communication(communication)
{}
// These methods are used by the Kernel class to manage the runtime of the custom hardware module instances that
// inherit from this base class.
/**
* @brief Queues the input command to be executed by the Module during the next runtime cycle iteration.
*
* @warning If the module already has a queued command, this method replaces that command with the input
* command data.
*
* @param command The command to execute.
* @param noblock Determines whether the queued command should run in blocking or non-blocking mode.
* @param cycle_delay The delay, in microseconds, before repeating (cycling) the command. Only provide this
* argument when queueing a recurrent command.
*/
void QueueCommand(const uint8_t command, const bool noblock, const uint32_t cycle_delay)
{
_execution_parameters.next_command = command;
_execution_parameters.next_noblock = noblock;
_execution_parameters.run_recurrently = true;
_execution_parameters.recurrent_delay = cycle_delay;
_execution_parameters.new_command = true;
}
/// Overloads the QueueCommand() method for queueing non-cyclic commands.
void QueueCommand(const uint8_t command, const bool noblock)
{
_execution_parameters.next_command = command;
_execution_parameters.next_noblock = noblock;
_execution_parameters.run_recurrently = false;
_execution_parameters.recurrent_delay = 0;
_execution_parameters.new_command = true;
}
/**
* @brief Resets the module's command queue.
*
* @note Calling this method does not abort already running commands: they are allowed to finish gracefully.
*/
void ResetCommandQueue()
{
_execution_parameters.next_command = 0;
_execution_parameters.next_noblock = false;
_execution_parameters.run_recurrently = false;
_execution_parameters.recurrent_delay = 0;
_execution_parameters.new_command = false;
}
/**
* @brief If possible, ensures that the module has an active command to execute.
*
* @note Uses the following order of preference to activate (execute) a command:
* finish already running commands > run new commands > repeat a previously executed recurrent command.
* When repeating recurrent commands, the method ensures the recurrent timeout has expired before reactivating
* the command.
*
* @returns bool @b true if the module has a command to execute and @b false otherwise.
*/
bool ResolveActiveCommand()
{
// If the command field is not 0, this means there is already an active command being executed and no
// further action is necessary.
if (_execution_parameters.command != 0) return true;
// If there is no active command and the next_command field is set to 0, this means that the module does
// not have any new or recurrent commands to execute.
if (_execution_parameters.next_command == 0) return false;
// If there is a next command in the queue and the new_command flag is set to true, activates the queued
// command without any further condition.
if (_execution_parameters.new_command)
{
// Transfers the command and the noblock flag from buffer fields to active fields
_execution_parameters.command = _execution_parameters.next_command;
_execution_parameters.noblock = _execution_parameters.next_noblock;
// Sets active command stage to 1, which is a secondary activation mechanism. All multi-stage commands
// should start with stage 1, as stage 0 is reserved for communicating no active commands sate.
_execution_parameters.stage = 1;
// Removes the new_command flag to indicate that the new command has been consumed.
_execution_parameters.new_command = false;
return true; // Returns true to indicate there is a command to run.
}
// If no new command is available, recurrent activation is enabled, and the requested recurrent_delay
// number of microseconds has passed, re-activates the previously executed command. Note, the
// next_command != 0 check is here to support correct behavior in response to Dequeue command, which sets
// the next_command field to 0 and should be able to abort cyclic and non-cyclic command execution.
if (_execution_parameters.run_recurrently &&
_execution_parameters.recurrent_timer > _execution_parameters.recurrent_delay &&
_execution_parameters.next_command != 0)
{
// Repeats the activation steps from above, minus the new_command flag modification (command is not new)
_execution_parameters.command = _execution_parameters.next_command;
_execution_parameters.noblock = _execution_parameters.next_noblock;
_execution_parameters.stage = 1;
return true; // Indicates there is a command to run.
}
// The only way to reach this point is to have a recurrent command with an unexpired recurrent delay timer.
// Returns false to indicate that no command was activated.
return false;
}
/**
* @brief Resets the module's command queue and aborts any currently running commands.
*/
void ResetExecutionParameters()
{
// Resets the _execution_parameters structure back to default values
_execution_parameters.command = 0;
_execution_parameters.stage = 0;
_execution_parameters.noblock = false;
_execution_parameters.next_command = 0;
_execution_parameters.next_noblock = false;
_execution_parameters.new_command = false;
_execution_parameters.run_recurrently = false;
_execution_parameters.recurrent_delay = 0;
_execution_parameters.recurrent_timer = 0;
_execution_parameters.delay_timer = 0;
}
/**
* @brief Returns the ID of the instance.
*/
[[nodiscard]]
uint8_t get_module_id() const
{
return _module_id;
}
/**
* @brief Returns the type (family ID) of the instance.
*/
[[nodiscard]]
uint8_t get_module_type() const
{
return _module_type;
}
/**
* @brief Returns the combined type and id value of the instance.
*/
[[nodiscard]]
uint16_t get_module_type_id() const
{
return _module_type_id;
}
/**
* @brief Sends an error message to notify the PC that the instance did not recognize the active command.
*/
void SendCommandActivationError() const
{
// Sends an error message that uses the unrecognized command code as 'command' and a 'not recognized' error
// code as the event.
SendData(static_cast<uint8_t>(kCoreStatusCodes::kCommandNotRecognized));
}
// These methods allow the Kernel class to interface with the custom logic of each custom hardware module
// instance, integrating them with the rest of the library components. The implementation of these methods
// relies on the end user as it has to be specific to each custom hardware module.
/**
* @brief Overwrites the memory of the object used to store the instance's runtime parameters with the data
* received from the PC.
*
* @note This method should call the ExtractParameters() method inherited from the base
* Module class to unpack the received custom parameters message into the structure (object) used to store the
* instance's custom runtime parameters.
*
* @returns true if new parameters were parsed successfully, false otherwise.
*/
virtual bool SetCustomParameters() = 0;
/**
* @brief Executes the instance method associated with the active command.
*
* @warning This method should not evaluate whether the command ran successfully, only whether the command
* was recognized and matched to the appropriate method call. The called method should use the inherited
* SendData() method to report command runtime status to the PC.
*
* @note This method should translate the active command returned by the get_active_command()
* method inherited from the base Module class into the call to the command-specific method that executes the
* command's logic.
*
* @returns true if the active module command was matched to a specific custom method, false otherwise.
*/
virtual bool RunActiveCommand() = 0;
/**
* @brief Sets up the instance's hardware and software assets.
*
* @note This method should set the initial (default) state of the instance's custom parameter structures and
* hardware (pins, timers, etc.).
*
* @attention Ideally, this method should not contain any logic that can fail or block, as this method is called
* as part of the initial library runtime setup procedure, before the communication interface is fully
* initialized.
*
* @returns true if the setup method ran successfully, false otherwise.
*/
virtual bool SetupModule() = 0;
/// Destroys the instance during cleanup.
virtual ~Module() = default;
protected:
// These methods are designed to help end users with writing custom module classes. They are not accessed by
// the Kernel class and are not required for integrating the custom module with the rest of the library. It is
// highly recommended to use these utility methods where appropriate, as they are required for the custom
// modules to support the full range of features provided by the library, such as non-blocking module command
// execution.
/**
* @brief Returns the active (running) command's code or 0, if there are no active commands.
*/
[[nodiscard]]
uint8_t get_active_command() const
{
return _execution_parameters.command;
}
/**
* @brief Terminates the active command (if any).
*
* If the aborted command is a recurrent command, the method resets the command queue to ensure that the
* command is not reactivated until it is re-queued from the PC.
*/
void AbortCommand()
{
// Only resets the command queue if there is no other command to replace the currently executed command when
// it is completed.
if (!_execution_parameters.new_command) ResetCommandQueue();
CompleteCommand(); // Finishes the command execution and sends the completion message to the PC.
}
/**
* @brief Advances the stage of the currently executed command.
*
* As part of its runtime, the method also resets the stage delay timer, making it a one-stop solution
* for properly transitioning between command stages.
*/
void AdvanceCommandStage()
{
_execution_parameters.stage++;
_execution_parameters.delay_timer = 0;
}
/**
* @brief Returns the execution stage of the active (running) command or 0, if there are no active
* commands.
*/
[[nodiscard]]
uint8_t get_command_stage() const
{
// If there is an actively executed command, returns its stage
if (_execution_parameters.command != 0) return _execution_parameters.stage;
// Otherwise returns 0 to indicate there is no actively running command
return 0;
}
/**
* @brief Completes (ends) the active (running) command's execution.
*
* @warning Only call this method when the command has completed everything it needed to do. To transition
* between the stages of the same command, use the AdvanceCommandStage() method instead.
*
* @note It is essential that this method is called at the end of every command's method (function) to allow
* executing other commands. Failure to do so can completely deadlock the Module and, in severe cases, the
* entire Microcontroller.
*/
void CompleteCommand()
{
// Resolves and, if necessary, notifies the PC that the active command has been completed. This is only done
// for commands that have finished their runtime. Specifically, recurrent commands do not report completion
// until they are canceled or replaced by a new command. One-shot commands always report completion.
if (_execution_parameters.new_command || _execution_parameters.next_command == 0 ||
!_execution_parameters.run_recurrently)
{
// Since this automatically accesses _execution_parameters.command for command code, this has to be
// called before resetting the command field.
SendData(static_cast<uint8_t>(kCoreStatusCodes::kCommandCompleted));
}
_execution_parameters.command = 0; // Removes active command code
_execution_parameters.stage = 0; // Secondary deactivation step, stage 0 is not a valid command stage
_execution_parameters.recurrent_timer =
0; // Resets the recurrent command timer when the command is completed
// If the command that has just been completed is not a recurrent command and there is no new command,
// resets the command queue to clear out the completed command data.
if (!_execution_parameters.new_command && !_execution_parameters.run_recurrently) ResetCommandQueue();
}
/**
* @brief Polls and (optionally) averages the value(s) of the specified analog pin.
*
* @param pin The analog pin to read.
* @param pool_size The number of pin readout values to average into the returned value. Set to 0 or 1 to
* disable averaging.
*
* @returns The read analog value.
*/
[[nodiscard]]
static uint16_t AnalogRead(const uint8_t pin, const uint16_t pool_size = 0)
{
uint16_t average_readout; // Pre-declares the final output readout
// Pool size 0 and 1 essentially mean the same: no averaging
if (pool_size < 2)
{
// If averaging is disabled, reads and outputs the acquired value.
average_readout = analogRead(pin);
}
else
{
uint32_t accumulated_readouts = 0; // Aggregates polled values by self-addition
// If averaging is enabled, repeatedly polls the pin the requested number of times.
for (auto i = decltype(pool_size) {0}; i < pool_size; i++)
{
accumulated_readouts += analogRead(pin); // Aggregates readouts
}
// Averages and rounds the final readout to avoid dealing with floating point math. This favors Arduino
// boards without an FP module, Teensies technically can handle floating point arithmetic just as
// efficiently. Adding pool_size/2 before dividing by pool_size forces half-up ('standard') rounding.
average_readout = static_cast<uint16_t>((accumulated_readouts + pool_size / 2) / pool_size);
}
return average_readout; // Returns the final averaged or raw readout
}
/**
* @brief Polls and (optionally) averages the value(s) of the specified digital pin.
*
* @param pin The digital pin to read.
* @param pool_size The number of pin readout values to average into the returned value. Set to 0 or 1 to
* disable averaging.
*
* @returns The read digital value as true (HIGH) or false (LOW).
*/
[[nodiscard]]
static bool DigitalRead(const uint8_t pin, const uint16_t pool_size = 0)
{
bool digital_readout; // Pre-declares the final output readout
// Reads the physical sensor value.
if (pool_size < 2)
{
digital_readout = digitalReadFast(pin);
}
else
{
uint32_t accumulated_readouts = 0; // Aggregates polled values by self-addition
// If averaging is enabled, repeatedly polls the pin the requested number of times. 'i' always uses
// the same type as pool_size.
for (auto i = decltype(pool_size) {0}; i < pool_size; i++)
{
accumulated_readouts += digitalReadFast(pin); // Aggregates readouts via self-addition
}
// Averages and rounds the final readout to avoid dealing with floating point math. This favors Arduino
// boards without an FP module, Teensies technically can handle floating point arithmetic just as
// efficiently. Adding pool_size/2 before dividing by pool_size forces half-up ('standard') rounding.
digital_readout = static_cast<bool>((accumulated_readouts + pool_size / 2) / pool_size);
}
return digital_readout;
}
/**
* @brief Delays the active command execution for the requested number of microseconds.
*
* @warning The delay is timed relative to the last command's execution stage advancement.
*
* @note Depending on the active command's configuration, the method can block in-place until the
* delay has passed or function as a non-blocking check for whether the required duration of microseconds has
* passed.
*
* @param delay_duration The delay duration, in microseconds.
*/
[[nodiscard]]
bool WaitForMicros(const uint32_t delay_duration) const
{
// If the caller command is executed in blocking mode, blocks in-place until the requested duration has
// passed
if (!_execution_parameters.noblock)
{
// Blocks until delay_duration has passed
while (_execution_parameters.delay_timer <= delay_duration);
}
// Evaluates whether the requested number of microseconds has passed. If the duration was enforced above,
// this check will always be true.
if (_execution_parameters.delay_timer >= delay_duration)
{
return true;
}
// If the requested duration has not passed, returns false
return false;
}
/**
* @brief Packages and sends the provided event_code and data object to the PC.
*
* The prototype code for the wire protocol is resolved automatically from the C++ type of the data object
* at compile time. Supports all 11 scalar types and C-style arrays at type-specific element counts up to
* the 248-byte payload cap. See the Supported SendData Types section of the README for the complete table
* of supported types and array sizes.
*
* @warning If sending the data fails for any reason, this method automatically emits an error message. Since
* that error message may itself fail to be sent, the method also statically activates the built-in LED of the
* board to visually communicate the encountered runtime error. Do not use the LED-connected pin or LED when
* using this method to avoid interference!
*
* @note If the message is intended to communicate only the event code, do not provide the data object.
* SendData() has an overloaded version specialized for sending event codes that is more efficient than the
* data-containing version.
*
* @tparam ObjectType The type of the data object to be sent along with the message.
* @param event_code The event that triggered the data transmission.
* @param object The data object to be sent along with the message.
*/
template <typename ObjectType>
void SendData(const uint8_t event_code, const ObjectType& object)
{
// Packages and sends the data to the connected system via the Communication class. If the message was sent,
// ends the runtime
if (_communication
.SendDataMessage(_module_type, _module_id, _execution_parameters.command, event_code, object))
return;
// If the message was not sent, calls a method that attempts to send a communication error message to the
// PC and turns on the built-in LED to visually indicate the error.
_communication.SendCommunicationErrorMessage(
_module_type,
_module_id,
_execution_parameters.command,
static_cast<uint8_t>(kCoreStatusCodes::kTransmissionError)
);
}
/**
* @brief Packages and sends the provided event code to the PC.
*
* This method overloads the SendData() method to optimize transmitting messages that only need to communicate
* the event.
*
* @param event_code The code of the event that triggered the data transmission.
*/
void SendData(const uint8_t event_code) const
{
// Packages and sends the data to the connected system via the Communication class. If the message was
// sent, ends the runtime
if (_communication.SendStateMessage(_module_type, _module_id, _execution_parameters.command, event_code))
return;
// If the message was not sent, calls a method that attempts to send a communication error message to the
// PC and turns on the built-in LED to visually indicate the error.
_communication.SendCommunicationErrorMessage(
_module_type,
_module_id,
_execution_parameters.command,
static_cast<uint8_t>(kCoreStatusCodes::kTransmissionError)
);
}
/**
* @brief Unpacks the instance's runtime parameters received from the PC into the specified storage object.
*
* @tparam ObjectType The type of the object used to store the PC-addressable module's parameters.
* @param storage_object The object used to store the PC-addressable module's parameters.
*
* @returns true if the parameters were successfully unpacked, false otherwise.
*/
template <typename ObjectType>
bool ExtractParameters(ObjectType& storage_object)
{
return _communication.ExtractModuleParameters(storage_object);
}
private:
/// Stores the instance's type (family) identifier code.
const uint8_t _module_type;
/// Stores the instance's unique identifier code.
const uint8_t _module_id;
/// Stores the instance's combined type and id uint16 code expected to be unique for each module instance
/// active at the same time.
const uint16_t _module_type_id = _module_type << 8 | _module_id;
/// Stores the Communication instance used to send module runtime data to the PC.
Communication& _communication;
/// Stores instance-specific runtime flow control parameters.
ExecutionControlParameters _execution_parameters;
};
#endif //AXMC_MODULE_H