westial / commotalkie

Communication SDK for environments with low power consumption requirements.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

CommoTalkie commotalkie icon

Communication interface for low power consumption devices. "Keep it tiny" is one of the most important requirements of this project.

In addition, an EByte integration library only for this project's required use cases.

Message

This communication pipeline validates, encrypts and transports the following Message structure.

message structure

MESSAGE_LENGTH value is a configuration parameter of "messageconfig.h" and it is the total length of the message, META and BODY together. A length change must consider the pragma alignment configuration of 4 set in ./include/message/Message.h

Signature

The most significant byte is the sign to validate the next message bytes. This byte is automatically set during the publication of a message.

Port

The second most significant byte is for feature segregation in the receiver.

Id

The third most significant byte purpose is an Id.

Content

The data to be transported.

SDK

Publisher and subscriber clients.

See ./include/application for the concrete interface and ./test for more implementation details.

Publish

This publisher sends a message towards a "topic". A topic is an address, event name, just a message destination.

// My Device Id
const unsigned char my_id = 0x06;
unsigned char body[MESSAGE_BODY_LENGTH];

// Initialize publisher
Publish_Create("salt", (const void *) push_fn);

// A port for the commotalkie context backend is as an endpoint for an HTTP API
const unsigned char port_for_temperature = 0x05;

// Send a message
Publish_Invoke((const unsigned char *)"destination::address", port_for_temperature, my_id, "a message");

// Send other message to a different port
Publish_Invoke((const unsigned char *)"destination::address", 0xAD, my_id, "another message");

// Send other message to another destination
Publish_Invoke((const unsigned char *)"another::destination", 0xAD, my_id, "and another message");

// Destroy
Publish_Destroy();

The push_fn function is a function with an implementation of the message submission using the kind of communication as your choice: LoRa, Wi-Fi, Bluetooth,...

Following that's the signature of the submission function. The maximum allowed size of the content is always the value of the constant MESSAGE_LENGTH. Address is just the place for the mentioned message destination (topic).

unsigned long push_fn(const unsigned char* address, const unsigned char* content, unsigned long size);

Pull

This subscriber listens for a message in its "topic" only, until a valid message is received or timeout expires. Timeout parameter as 0 disables the expiration.

// Initialize the content destination variables
const unsigned long timeout_milliseconds = 10 * 1000;
const unsigned char to_id = 0xAA;

// Initialize pulling messages to ID 0xAA only
Pull_Create(
    "salt",
    (void *) listen_fn,
    (void *) now_fn,
    timeout_milliseconds,
    &to_id
);

// Start listening for messages sent to "destination::address" only
unsigned char body[MESSAGE_BODY_LENGTH];
unsigned char port, id;
result = Pull_Invoke((const unsigned char *)"destination::address", &port, &id, body);

// Do some stuff with the message contents
something(body);

// Destroy after first message
Pull_Destroy();

The option to_id validates the messages sent to the given ID only. If this parameter value is 0x00, the pulling does validate a message sent to any ID. Initialization example for wildcard ID:

Pull_Create(
    "salt",
    (const void *) listen_fn,
    (const void *) now_fn,
    (const void *) turn_on_fn,
    (const void *) turn_off_fn,
    timeout_at,
    0x00
);

There is a salt value in the first argument.

The timeout_at argument value is the listening expiration time. The unit for this value depends on the value returned by the time service now_fn. If the timeout_at value is 0, there is no timeout and the device is listening permanently.

The last argument is an ID to make the listener selective. If a received message is not forwarded to this ID, the message is discarded.

The remaining arguments are the following function:

  • Listen for a message whatever port or device the implementation uses.
  • A time service providing a time in a number type value.
  • A command function to turn on the listener.
  • A command function to turn off the listener.

The listening function listens for a message input from whatever communication device is provided in the integration, fills up the buffer with and returns one of the following integer values:

  • 1 on success.
  • 0 when it did receive nothing.
  • -1 on error.

The listening function has the following signature. The size argument is exactly the value of MESSAGE_LENGTH. If the transporter driver prepends or appends bytes to the message for technical requirements, the listen_fn fills up the buffer argument with the content without the transporter additions.

int listen_fn(const unsigned char* address, unsigned char* buffer, const unsigned long size);

The time service function signature is pretty simple, it's just a call to a clock to get an increasing number of milliseconds:

unsigned long now_fn();

The signature for the turn_on_fn is as follows:

void turn_on_fn();

The signature for the turn_off_fn is as follows:

void turn_off_fn();

The listener is turned off by default and turned on for listening during the window limited by the timeout only.

Builders make it easier

A builder for Pull and Publish instances makes their implementation easier and safer. Using the following examples to create the instances is highly recommended.

More examples here:

Publish Builder

The result of PublisherBuilder_Build() is 1 or 0 for success or error respectively. Salt and sending callback are required.

Create an instance of Publish and use it as follows:

PublisherBuilder_Create();
PublisherBuilder_SetSalt(salt);
PublisherBuilder_SetSendCallback(push_fn);
if (!PublisherBuilder_Build()) exit(-1);
PublisherBuilder_Destroy();

Publish_Invoke((const unsigned char*)"destination::address:1", 0x05, 0x06, body);
Publish_Invoke((const unsigned char*)"destination::address:2", 0x06, 0x06, body);
Publish_Invoke((const unsigned char*)"destination::address:3", 0x07, 0x06, body);

// Destroy if you need it no more.
Publish_Destroy();

Pull Builder

The result of SubscriberBuilder_Build() is 1 or 0 for success or error respectively. Salt and listening callback are required.

Create an instance of Pull and use it as follows:

SubscriberBuilder_Create();
SubscriberBuilder_SetSalt("salt");
SubscriberBuilder_SetListenCallback(listen_fn);
SubscriberBuilder_SetReceiverStateCallback(turn_on_fn, turn_off_fn);
SubscriberBuilder_SetId(&id);
if (!SubscriberBuilder_Build()) exit(-1);
SubscriberBuilder_Destroy();

Pull_Invoke((const unsigned char*)"destination::address", &port, &id, body);
Pull_Invoke((const unsigned char*)"destination::address", &port, &id, body);
Pull_Invoke((const unsigned char*)"destination::address", &port, &id, body);

// Destroy if you need it no more.
Pull_Destroy();

Testing

Testing directory ./test is the best documentation stuff of this project. There are test scenarios for all components separately and together injected into a use case.

It is created from TDD based on the book "Test-Driven Development for Embedded C" written by the great James W. Grenning.

The tests are handled by the library CppUTest, also following the guidelines from the book mentioned above and the documentation in the official page of the library.

Driver integration

The publishing and pulling use cases have some dependencies. Many of them depend on a real communication device: bluetooth, Wi-fi, LoRa,...

Following the low power is better directive, I'm focused on LoRa technologies for this context. And I integrated an EByte transmitter module via Driver interface. See include/driver/Driver.h and its unit test groups.

The src/driver/EByteDriver.c integration keeps the power consumption as low as possible. It supports a few parameterized features like air data rate, transmission mode and transmission power. It keeps the device is sleep mode by default and only moves to active state for sending or receiving messages.

EByte Driver example

See the following examples about how to use the EByte integration Driver.

Create a Driver instance

Timer timer = Timer_Create((const void *)stub_progressive_epoch_ms_fn);
unsigned long default_timeouts[2];
default_timeouts[MODE_TIMEOUT_INDEX] = 5 * 1000;
default_timeouts[SERIAL_TIMEOUT_INDEX] = 2 * 1000;

PinMap pins;
RadioParams params;
IOCallback io;

pins.m0 = 1;
pins.m1 = 2;
pins.aux = 3;
params.address[0] = topic[DRIVER_ADDRESS_HIGH_INDEX];
params.address[1] = topic[DRIVER_ADDRESS_LOW_INDEX];
params.channel = topic[2];
params.air_data_rate = air_data_rate;
params.is_fixed_transmission = is_fixed;
params.full_transmission_power = full_power;
io.read_pin = read_pin;
io.write_pin = write_pin;
io.write_to_serial = write_to_serial;
io.read_from_serial = read_from_serial;
io.clear_serial = clear_serial;

Driver_Create(pins, &params, &io, &timer, default_timeouts);

The time service function signature for now_fn() is just the same of the mentioned in the Pull use case above.

First value in default_timeouts defines the number of time units, milliseconds in this example, to wait until expiration for AUX getting high again after changing the EByte switch mode. And the second default_timeouts value is the same for the serial transmission. Both, the switching mode and the serial transmission usually spend a few milliseconds to get AUX high again, so this timeout is rarely reached. Although, if any of these timeouts expire, the Driver instance gets WARNING as state. The WARNING state does not block a feature, it's informative only and it can be changed next time the switching mode is requested.

PinMap defines the UART EByte module to MCU device pinout relationship. In the example above, the pin M0 of the EByte module is connected to the MCU pin #1, and so on.

The IOCallback method signatures are clearly defined in its definition:

typedef struct IOCallback {
  int (*read_pin)(unsigned char);
  void (*write_pin)(unsigned char, unsigned char);
  unsigned long (*write_to_serial)(unsigned char *, unsigned long);
  unsigned long (*read_from_serial)(unsigned char *, unsigned long, unsigned long);
  void (*clear_serial)();
} IOCallback;

Send with a Driver instance

Once the Driver instance is created by the example above, it follows up with a string submission.

const unsigned char raw_message[] = "abcdefghi";
Destination target;
target.address_high = 0xA1;
target.address_low = 0xA2;
target.channel = 0xA3;
unsigned long written =
  Driver_Send(&sample_driver, &target, raw_message, sizeof(raw_message));

In the example above, written gets the number of bytes sent.

Receive with a Driver instance

Once the Driver instance is created, let it listen to messages from other LoRa devices.

unsigned char buffer[8];
int result = Driver_Receive(&sample_driver, buffer, sizeof(buffer));

EByte Driver instance listens the transmission until one of the next conditions is met:

  • The EByte module AUX pin gets high, according to its documentation.
  • The received content fills up the given buffer.
  • The Serial timeout mentioned above expires, and it sets Driver instance state as WARNING.

Support for Power Management Unit

There is also an interface implementation here include/sleepable for an external project of a Power Management Unit for an unstable supply.

CommoTalkino Full integration example

https://github.com/westial/commotalkino is an example of this SDK and EByte driver integration. That project consists on permanently sending and receiving a number that is getting incremented every transmission between a pair of Arduino MCU.

It pretends to be a funny virtual ping pong match but indeed it's a hard stress test to check the stability and reliability of this whole communication project (uahaha!).

Arduino Library

There is an Arduino library based implementation in ./arduino.

To prepare the files for this Arduino library, go to this directory and execute make. It creates a subdirectory in ./arduino/prebuild with the directory of the library itself ./arduino/prebuild/CommoTalkie

To install the library move the ./arduino/prebuild/CommoTalkie to your OS Arduino Libraries directory and ./arduino/prebuild/CommoTalkie/messageconfig.h to the project you want to use this library in.

License

GNU General Public License (GPLv3). Read the attached license file.

Author

Jaume Mila Bea jaume@westial.com

About

Communication SDK for environments with low power consumption requirements.

License:GNU General Public License v3.0


Languages

Language:C 88.5%Language:CMake 9.5%Language:Makefile 2.0%