Skip to content

Commit

Permalink
Merge pull request #17 from martijnvandermarel/microapp-sdk-refactor
Browse files Browse the repository at this point in the history
Microapp sdk refactor
  • Loading branch information
vliedel authored Aug 24, 2022
2 parents cff0dc5 + e2b288f commit 39e1ec7
Show file tree
Hide file tree
Showing 42 changed files with 1,435 additions and 1,211 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ include $(TARGET_CONFIG_FILE)
include config.mk
-include private.mk

SOURCE_FILES=include/startup.S src/main.c src/microapp.c src/Arduino.c src/Wire.cpp src/Serial.cpp src/ArduinoBLE.cpp src/BleUtils.cpp src/BleDevice.cpp src/Mesh.cpp src/CrownstoneDimmer.cpp src/CrownstoneRelay.cpp $(SHARED_PATH)/ipc/cs_IpcRamData.c $(TARGET).c
SOURCE_FILES=include/startup.S src/main.c src/microapp.c src/Arduino.c src/Wire.cpp src/Serial.cpp src/ArduinoBLE.cpp src/BleUtils.cpp src/BleDevice.cpp src/Mesh.cpp src/CrownstoneDimmer.cpp src/CrownstoneRelay.cpp src/ServiceData.cpp $(SHARED_PATH)/ipc/cs_IpcRamData.c $(TARGET).c

# First initialize, then create .hex file, then .bin file and file end with info
all: init $(TARGET).hex $(TARGET).bin $(TARGET).info
Expand Down
210 changes: 210 additions & 0 deletions docs/CONTROL_FLOW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# Control flow

The control flow between bluenet and the microapp can in essence be described as follows (ignoring the initialization and `setup` for the moment):

Once every microapp tick, bluenet yields control towards the microapp, hoping that at some point it will yield back. The microapp will call its `loop` function. Often, the loop function will perform some task that requires bluenet, e.g. printing something to serial. Microapp will perform what can be thought of as a 'soft' yield: control is yielded back to bluenet to handle the request (e.g. sending the serial payload to uart), but the microapp expects to be handed back control once bluenet finishes handling the request. Upon completion of `loop` (or on a `delay` call within `loop`) the microapp will do a 'hard' yield: it yields without the expectations of being handed back control right away. Bluenet will then stop handing control to the microapp until the next microapp tick.

Interrupts function almost the exact same way as described above, with the exception that the initial trigger to enter the microapp comes from some predefined event within bluenet other than a tick.

In most cases, bluenet functions exactly as described above. However, there are some exceptions.
Firstly, if the microapp is doing too many consecutive soft yields, bluenet will not hand control back to the microapp after handling the request. Instead, the microapp is only called again next microapp tick or interrupt.
Secondly, bluenet throttles how many interrupts are passed through to the microapp per tick. Only after the next microapp tick will the microapp be able to receive interrupts again.
Thirdly, the microapp has a max number of 'nested' interrupts it can handle, since it has to store the context of each layer on a stack. At a certain depth it will drop incoming interrupts.
Upon a dropped interrupt, bluenet will not generate interrupts again until the microapp finishes existing interrupt handlers.
Lastly, though this is not a part of the main control flow, a watchdog in bluenet will keep track when a microapp gets stuck or takes up too much time. It will then reboot and disable the microapp. These mechanisms together should ensure that the microapp cannot compromise the internal working of bluenet and the crownstone in general.

## Context stacking
Every time a new interrupt comes in, the microapp will handle it immediately.
However, it will also want to store the original content of the shared buffers. This is important because there may be request return values in these buffers that the microapp has not handled yet. Also, the interrupt content from bluenet needs to be copied from the shared buffer because at any time bluenet may overwrite it with new interrupts.
Hence, before handling a new interrupt, the microapp will copy the contents of the shared buffers to an internal stack.
Once it finishes handling the interrupt, the top buffer can be popped from the stack and back to the shared buffer.
In most common use cases, an interrupt will be handled and return before bluenet generates another interrupt. However, when an interrupt handler generates too many consecutive requests, or contains async calls, bluenet may generate an interrupt before the previous one is finished. This leads to nested interrupts.
The microapp limits the maximum amount of concurrent interrupts via the maximum stack height. If the stack is full when a new interrupt is generated, the interrupt is dropped.

## Minimal example
Let's consider the following `loop()` in the microapp:
```
void loop() {
Serial.println("Hello");
}
```
This is what is happening under the hood:

```mermaid
sequenceDiagram
participant b as Bluenet
participant c as Coroutine
participant m as Microapp Library
participant um as User-facing Microapp
participant m2b as MicroappToBluenetBuffer
participant b2m as BluenetToMicroappBuffer
Note over b : A bluenet tick is the initial trigger.
b ->> b : tickMicroapp()
b -->> b2m : Write to shared buffer
Note over b2m : messageType = CONTINUE <br> ack = NO_REQUEST
b ->> b : callMicroapp()
b ->> c : nextCoroutine()
Note over c,m : Resume in the sendMessage() call <br> of signalLoopEnd() of previous loop().
c ->> m : enter microapp
m ->> m : handleBluenetInterrupt()
b2m -->> m : Read from shared buffer
Note over m : handleBluenetInterrupt() returns <br> early because of NO_REQUEST.
m ->> um : loop()
um ->> m : Serial.println("Hello")
m -->> m2b : Write to shared buffer
Note over m2b: messageType = SERIAL <br> ack = REQUEST
m ->> m : sendMessage()
m ->> c : microapp_callback()
c ->> b : yieldCoroutine()
b ->> b : handleAck()
b2m -->> b : Read from shared buffer
Note over b : handleAck() confirms request should be handled <br> because of NO_REQUEST from bluenet
b ->> b : handleRequest()
m2b -->> b : Read from shared buffer
Note over b : In handleRequest() the <br> serial print request is handled.
b -->> m2b : Write to shared buffer
Note over m2b : ack = SUCCESS
b ->> b : stopAfterMicroappRequest()
Note over b : stopAfterMicroappRequest() will <br> return false for messageType SERIAL. <br> Hence, call microapp again.
b ->> b : callMicroapp()
b ->> c : nextCoroutine()
Note over c,m : Resume in the sendMessage() call <br> of Serial.println().
c ->> m : enter microapp
m ->> m : handleBluenetInterrupt()
b2m -->> m : Read from shared buffer
Note over m : handleBluenetInterrupt() returns <br> early because of NO_REQUEST.
m2b -->> m : Read from shared buffer
Note over m : Check ack from bluenet to see if <br> serial request was successfull
m ->> um : Serial.println() returns
um ->> m : loop() returns
m ->> m : signalLoopEnd()
m -->> m2b : Write to shared buffer
Note over m2b : messageType = YIELD <br> ack = NO_REQUEST
m ->> m : sendMessage()
m ->> c : microapp_callback()
c ->> b : yieldCoroutine()
b ->> b : handleAck()
b2m -->> b : Read from shared buffer
Note over b : handleAck() confirms request should be handled <br> because of NO_REQUEST from bluenet
b ->> b : handleRequest()
m2b -->> b : Read from shared buffer
Note over b : handleRequest() does <br> nothing for messageType YIELD.
b -->> m2b : Write to shared buffer
Note over m2b : ack = SUCCESS
b ->> b : stopAfterMicroappRequest()
Note over b : stopAfterMicroappRequest() will <br> return true for messageType YIELD. <br> Hence, do not call microapp again.
Note over b : tickMicroapp() ends.
```
This sequence of events can be visualized as follows.
![simple loop](/docs/gifs/serial_in_loop.gif)

## Interrupt example
Now, consider that an interrupt handler has been registered for e.g. incoming mesh messages. A very simple microapp for this could look like this:

```
void callback(MeshMsg msg) {
Serial.println("Hello");
}
void setup() {
Mesh.setIncomingMeshMsgHandler(callback);
Mesh.listen();
}
void loop() {
// empty
}
```
The following sequence diagram shows what will happen when a mesh message of the microapp type is received in bluenet. Note that the original contents of the microapp request buffer are stored on the stack and restored after handling the interrupts so that the microapp will continue with the next tick call exactly in the same state as it was before the interrupt.

```mermaid
sequenceDiagram
participant b as Bluenet
participant c as Coroutine
participant m as Microapp Library
participant um as User-facing Microapp
participant m2b as MicroappToBluenetBuffer
participant b2m as BluenetToMicroappBuffer
Note over b : An event of type EVT_RECV_MESH_MSG <br> is the initial trigger.
b ->> b : handleEvent()
b ->> b : onReceivedMeshMessage()
Note over b : If the mesh message is not of <br> the microapp type, return early.
b -->> b2m : Write to shared buffer
Note over b2m : messageType = MESH <br> ack = REQUEST
b ->> b : generateInterrupt()
b ->> b : callMicroapp()
b ->> c : nextCoroutine()
Note over c,m : Resume in the sendMessage() call <br> of signalLoopEnd() of previous loop().
c ->> m : enter microapp
m ->> m : handleBluenetInterrupt()
b2m -->> m : Read from shared buffer
alt interrupt stack full
Note over m : If the interrupt stack is full, <br> the microapp drops the interrupt.
m -->> b2m : Write to shared buffer
Note over b2m : ack = ERR_BUSY
m ->> m : sendMessage()
m ->> c : microapp_callback()
c ->> b : yieldCoroutine()
b ->> b : handleAck()
b2m -->> b : Read from shared buffer
Note over b : handleAck() will recognize <br> the microapp dropped the interrupt, <br> and will not call it again <br> until the microapp yields <br> or finishes handling an interrupt. <br> Microapp request is ignored.
Note over b : generateInterrupt() ends.
else interrupt stack not full
Note over m : If there is space in the interrupt stack, <br> acknowledge bluenets interrupt
m -->> b2m : Write to shared buffer
Note over b2m : ack = IN_PROGRESS
Note over m : handleBluenetInterrupt() copies <br> shared buffers to top of <br> request- and interrupt stacks.
m ->> m : handleInterrupt()
Note over m : handleInterrupt() identifies <br> the interrupt handler based on <br> messageType = MESH and <br> internal data of the mesh message.
m ->> m : handleMeshInterrupt()
m ->> um : callback()
um ->> m : Serial.println("Hello")
m -->> m2b : Write to shared buffer
Note over m2b: messageType = SERIAL <br> ack = REQUEST
m ->> m : sendMessage()
m ->> c : microapp_callback()
c ->> b : yieldCoroutine()
b ->> b : handleAck()
b2m -->> b : Read from shared buffer
Note over b : On an IN_PROGRESS, <br> continue the interrupt.
b ->> b : handleRequest()
m2b -->> b : Read from shared buffer
Note over b : In handleRequest() the <br> serial print request is handled.
b -->> m2b : Write to shared buffer
Note over m2b : ack = SUCCESS
b ->> b : stopAfterMicroappRequest()
Note over b : stopAfterMicroappRequest() will <br> return false for messageType SERIAL. <br> Hence, call microapp again.
b ->> b : callMicroapp()
b ->> c : nextCoroutine()
Note over c,m : Resume in the sendMessage() call <br> of Serial.println().
c ->> m : enter microapp
m ->> m : handleBluenetInterrupt()
b2m -->> m : Read from shared buffer
Note over m : handleBluenetInterrupt() returns <br> early because ack is not REQUEST.
m2b -->> m : Read from shared buffer
Note over m : Check ack from bluenet to see if <br> serial request was successfull
m ->> um : Serial.println() returns
um ->> m : callback() returns
m ->> m : handleMeshInterrupt() returns
Note over m : The user handler or internal handler <br> may return a return code, e.g. SUCCESS
m ->> m : handleInterrupt() returns
Note over m : Continue in handleBluenetRequest()
Note over m : Clear interrupt stack entry <br> and copy top request stack entry <br> back to shared buffer
m -->> m2b : Write to shared buffer
Note over m2b : messageType = YIELD <br> ack = SUCCESS
m -->> b2m : Write to shared buffer
Note over b2m : ack = SUCCESS
m ->> m : sendMessage()
m ->> c : microapp_callback()
c ->> b : yieldCoroutine()
b ->> b : handleAck()
b2m -->> b : Read from shared buffer
Note over b : handleAck() recognizes successfull <br> handling of interrupt. <br> Microapp request is ignored.
Note over b : generateInterrupt() ends.
end
```

This sequence of events can be visualized as follows.
![simple interrupt](/docs/gifs/serial_in_interrupt.gif)
105 changes: 0 additions & 105 deletions docs/README.md

This file was deleted.

File renamed without changes.
Binary file added docs/gifs/serial_in_interrupt.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/gifs/serial_in_loop.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 7 additions & 9 deletions examples/basic.ino
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@
//

#include <Arduino.h>
#include <ServiceData.h>

// Show how a counter is incremented
static int counter = 100;

microapp_service_data_t serviceData;

volatile byte state = LOW;
volatile byte state2 = LOW;

//
const size_t serviceDataSize = 1;
const uint16_t serviceDataUuid = 0x1234;
uint8_t serviceData[serviceDataSize];

// The blink function will be called on interrupt CHANGE. This means when you press and when you release a button.
// Therefore two state variables are used to only toggle the state every two interrupts.
void blink() {
Expand All @@ -38,10 +40,6 @@ void setup() {
// We can also write integers.
Serial.println(counter);

// Set the UUID of this microapp.
serviceData.appUuid = 0x1234;
serviceData.dlen = 1;

// Set LED pin to OUTPUT, so we can write.
pinMode(LED2_PIN, OUTPUT);

Expand Down Expand Up @@ -108,6 +106,6 @@ void loop() {
Serial.println(counter);

// Let's advertise the counter value in the service data.
serviceData.data[0] = counter;
SerialServiceData.write(&serviceData);
serviceData[0] = counter;
ServiceData.write(serviceDataUuid, serviceData, serviceDataSize);
}
Loading

0 comments on commit 39e1ec7

Please sign in to comment.