GithubHelp home page GithubHelp logo

bxparks / aceroutine Goto Github PK

View Code? Open in Web Editor NEW
170.0 11.0 18.0 2.54 MB

A low-memory, fast-switching, cooperative multitasking library using stackless coroutines on Arduino platforms.

License: MIT License

C 1.79% C++ 96.50% Makefile 1.71%

aceroutine's Introduction

AceRoutine

AUnit Tests

NEW: Profiling in v1.5: Version 1.5 adds the ability to profile the execution time of Coroutine::runCoroutine() and render the histogram as a table or a JSON object. See Coroutine Profiling for details.

A low-memory, fast-switching, cooperative multitasking library using stackless coroutines on Arduino platforms.

This library is an implementation of the ProtoThreads library for the Arduino platform. It emulates a stackless coroutine that can suspend execution using a yield() or delay() functionality to allow other coroutines to execute. When the scheduler makes its way back to the original coroutine, the execution continues right after the yield() or delay().

There are only 2 core classes in this library:

  • Coroutine class provides the context variables for all coroutines
  • CoroutineScheduler class handles the scheduling (optional)

The following classes are used for profiling:

  • CoroutineProfiler interface
  • LogBinProfiler provides an implementation that tracks the execution time in 32 logarithmic bins from 1us to 4295s.
  • LogBinTableRenderer prints the histogram as a table
  • LogBinJsonRenderer prints the histogram as a JSON object

The following is an experimental feature whose API and functionality may change considerably in the future:

  • Channel class allows coroutines to send messages to each other

The library provides a number of macros to help create coroutines and manage their life cycle:

  • COROUTINE(): defines an instance of the Coroutine class or an instance of a user-defined subclass of Coroutine
  • COROUTINE_BEGIN(): must occur at the start of a coroutine body
  • COROUTINE_END(): must occur at the end of the coroutine body
  • COROUTINE_YIELD(): yields execution back to the caller, often CoroutineScheduler but not necessarily
  • COROUTINE_AWAIT(condition): yield until condition becomes true
  • COROUTINE_DELAY(millis): yields back execution for millis. The millis parameter is defined as a uint16_t.
  • COROUTINE_DELAY_MICROS(micros): yields back execution for micros. The micros parameter is defined as a uint16_t.
  • COROUTINE_DELAY_SECONDS(seconds): yields back execution for seconds. The seconds parameter is defined as a uint16_t.
  • COROUTINE_LOOP(): convenience macro that loops forever
  • COROUTINE_CHANNEL_WRITE(channel, value): writes a value to a Channel
  • COROUTINE_CHANNEL_READ(channel, value): reads a value from a Channel

Here are some of the compelling features of this library compared to others (in my opinion of course):

  • low memory usage
    • 8-bit (e.g. AVR) processors:
      • the first Coroutine consumes about 230 bytes of flash
      • each additional Coroutine consumes 170 bytes of flash
      • each Coroutine consumes 16 bytes of static RAM
      • CoroutineScheduler consumes only about 40 bytes of flash and 2 bytes of RAM independent of the number of coroutines
    • 32-bit (e.g. STM32, ESP8266, ESP32) processors
      • the first Coroutine consumes between 120-450 bytes of flash
      • each additional Coroutine consumes about 130-160 bytes of flash,
      • each Coroutine consumes 28 bytes of static RAM
      • CoroutineScheduler consumes only about 40-60 bytes of flash and 4 bytes of static RAM independent of the number of coroutines
  • extremely fast context switching
    • Direct Scheduling (call Coroutine::runCoroutine() directly)
      • ~1.0 microseconds on a 16 MHz ATmega328P
      • ~0.4 microseconds on a 48 MHz SAMD21
      • ~0.3 microseconds on a 72 MHz STM32
      • ~0.3 microseconds on a 80 MHz ESP8266
      • ~0.1 microseconds on a 240 MHz ESP32
      • ~0.17 microseconds on 96 MHz Teensy 3.2 (depending on compiler settings)
    • Coroutine Scheduling (use CoroutineScheduler::loop()):
      • ~5.2 microseconds on a 16 MHz ATmega328P
      • ~1.3 microseconds on a 48 MHz SAMD21
      • ~0.9 microseconds on a 72 MHz STM32
      • ~0.8 microseconds on a 80 MHz ESP8266
      • ~0.3 microseconds on a 240 MHz ESP32
      • ~0.4 microseconds on 96 MHz Teensy 3.2 (depending on compiler settings)
  • uses the computed goto feature of the GCC compiler (also supported by Clang) to avoid the Duff's Device hack
    • allows switch statements in the coroutines
  • C/C++ macros eliminate boilerplate code and make the code easy to read
  • the base Coroutine class is easy to subclass to add additional variables and functions
  • fully unit tested using AUnit

Some limitations are:

  • A Coroutine cannot return any values.
  • A Coroutine is stackless and therefore cannot preserve local stack variables across multiple calls. Often the class member variables or function static variables are reasonable substitutes.
  • Coroutines are designed to be statically allocated, not dynamically created and destroyed on the heap. Dynamic memory allocation on an 8-bit microcontroller with 2kB of RAM would cause too much heap fragmentation. And the virtual destructor pulls in malloc() and free() which increases flash memory by 600 bytes on AVR processors.
  • A Channel is an experimental feature and has limited features. It is currently an unbuffered, synchronized channel. It can be used by only one reader and one writer.

After I had completed most of this library, I discovered that I had essentially reimplemented the <ProtoThread.h> library in the Cosa framework. The difference is that AceRoutine is a self-contained library that works on any platform supporting the Arduino API (AVR, Teensy, ESP8266, ESP32, etc), and it provides a handful of additional macros that can reduce boilerplate code.

Version: 1.5.1 (2022-09-20)

Changelog: CHANGELOG.md

Table of Contents

Hello Coroutines

HelloCoroutine

This is the HelloCoroutine.ino sample sketch which uses the COROUTINE() macro to automatically handle a number of boilerplate code, and some internal bookkeeping operations. Using the COROUTINE() macro works well for relatively small and simple coroutines.

#include <AceRoutine.h>
using namespace ace_routine;

const int LED = LED_BUILTIN;
const int LED_ON = HIGH;
const int LED_OFF = LOW;

COROUTINE(blinkLed) {
  COROUTINE_LOOP() {
    digitalWrite(LED, LED_ON);
    COROUTINE_DELAY(100);
    digitalWrite(LED, LED_OFF);
    COROUTINE_DELAY(500);
  }
}

COROUTINE(printHelloWorld) {
  COROUTINE_LOOP() {
    Serial.print(F("Hello, "));
    Serial.flush();
    COROUTINE_DELAY(1000);
    Serial.println(F("World"));
    COROUTINE_DELAY(4000);
  }
}

void setup() {
  delay(1000);
  Serial.begin(115200);
  while (!Serial); // Leonardo/Micro
  pinMode(LED, OUTPUT);
}

void loop() {
  blinkLed.runCoroutine();
  printHelloWorld.runCoroutine();
}

The printHelloWorld coroutine prints "Hello, ", waits 1 second, then prints "World", then waits 4 more seconds, then repeats from the start. At the same time, the blinkLed coroutine blinks the builtin LED on and off, on for 100 ms and off for 500 ms.

HelloScheduler

The HelloScheduler.ino sketch implements the same thing using the CoroutineScheduler:

#include <AceRoutine.h>
using namespace ace_routine;

... // same as above

void setup() {
  delay(1000);
  Serial.begin(115200);
  while (!Serial); // Leonardo/Micro
  pinMode(LED, OUTPUT);

  CoroutineScheduler::setup();
}

void loop() {
  CoroutineScheduler::loop();
}

The CoroutineScheduler can automatically manage all coroutines defined by the COROUTINE() macro, which eliminates the need to itemize your coroutines in the loop() method manually. Unfortunately, this convenience is not free (see MemoryBenchmark):

  • The CoroutineScheduler singleton instance increases the flash memory by about 110 bytes.
  • The CoroutineScheduler::loop() method calls the Coroutine::runCoroutine() method through the virtual dispatch instead of directly, which is slower and takes more flash memory.
  • Each Coroutine instance consumes an additional ~70 bytes of flash when using the CoroutineScheduler.

On 8-bit processors with limited memory, the additional resource consumption can be important. On 32-bit processors with far more memory, these additional resources are often inconsequential. Therefore the CoroutineScheduler is recommended mostly on 32-bit processors.

HelloManualCoroutine

The HelloManualCoroutine.ino program shows what the code looks like without the convenience of the COROUTINE() macro. For more complex programs, with more than a few coroutines, especially if the coroutines need to communicate with each other, this coding structure can be more powerful.

#include <Arduino.h>
#include <AceRoutine.h>
using namespace ace_routine;

const int LED = LED_BUILTIN;
const int LED_ON = HIGH;
const int LED_OFF = LOW;

class BlinkLedCoroutine: public Coroutine {
  public:
    int runCoroutine() override {
      COROUTINE_LOOP() {
        digitalWrite(LED, LED_ON);
        COROUTINE_DELAY(100);
        digitalWrite(LED, LED_OFF);
        COROUTINE_DELAY(500);
      }
    }
};

class PrintHelloWorldCoroutine: public Coroutine {
  public:
    int runCoroutine() override {
      COROUTINE_LOOP() {
        Serial.print(F("Hello, "));
        Serial.flush();
        COROUTINE_DELAY(1000);
        Serial.println(F("World"));
        COROUTINE_DELAY(4000);
      }
    }
};

BlinkLedCoroutine blinkLed;
PrintHelloWorldCoroutine printHelloWorld;

void setup() {
  delay(1000);
  Serial.begin(115200);
  while (!Serial); // Leonardo/Micro
  pinMode(LED, OUTPUT);
}

void loop() {
  blinkLed.runCoroutine();
  printHelloWorld.runCoroutine();
}

HelloCoroutineWithProfiler

Version 1.5 added support for profiling the execution time of Coroutine::runCoroutine() through the CoroutineProfiler interface. Currently only a single implementation (LogBinProfiler) is provided.

The HelloCoroutineWithProfiler.ino program shows how to setup the profilers and extract the profiling information using the Coroutine::runCoroutineWithProfiler() instead of the usual Coroutine::runCoroutine():

#include <AceRoutine.h>
using namespace ace_routine;

const int PIN = 2;
const int LED = LED_BUILTIN;
const int LED_ON = HIGH;
const int LED_OFF = LOW;

COROUTINE(blinkLed) {
  COROUTINE_LOOP() {
    digitalWrite(LED, LED_ON);
    COROUTINE_DELAY(100);
    digitalWrite(LED, LED_OFF);
    COROUTINE_DELAY(500);
  }
}

COROUTINE(printHelloWorld) {
  COROUTINE_LOOP() {
    Serial.print(F("Hello, "));
    Serial.flush();
    COROUTINE_DELAY(1000);
    Serial.println(F("World"));
    COROUTINE_DELAY(4000);
  }
}

COROUTINE(printProfiling) {
  COROUTINE_LOOP() {
    LogBinTableRenderer::printTo(
        Serial, 3 /*startBin*/, 14 /*endBin*/, false /*clear*/);
    LogBinJsonRenderer::printTo(
        Serial, 3 /*startBin*/, 14 /*endBin*/);

    COROUTINE_DELAY(5000);
  }
}

LogBinProfiler profiler1;
LogBinProfiler profiler2;
LogBinProfiler profiler3;

void setup() {
  delay(1000);
  Serial.begin(115200);
  while (!Serial); // Leonardo/Micro

  pinMode(LED, OUTPUT);
  pinMode(PIN, INPUT);

  // Coroutine names can be either C-string or F-string.
  blinkLed.setName("blinkLed");
  readPin.setName(F("readPin"));

  // Manually attach the profilers to the coroutines.
  blinkLed.setProfiler(&profiler1);
  readPin.setProfiler(&profiler2);
  printProfiling.setProfiler(&profiler3);
}

void loop() {
  blinkLed.runCoroutineWithProfiler();
  printHelloWorld.runCoroutineWithProfiler();
  printProfiling.runCoroutineWithProfiler();
}

Every 5 seconds, the printProfiling coroutine will print the profiling information in 2 formats on the Serial port:

  • a formatted table through the LogBinTableRenderer
  • a JSON object using the LogBinJsonRenderer
name         <16us <32us <64us<128us<256us<512us  <1ms  <2ms  <4ms  <8ms    >>
0x1DB        16921 52650     0     0     0     0     0     0     0     0     1
readPin      65535  1189     0     0     0     0     0     0     0     0     0
blinkLed     65535   830     0     0     0     0     0     0     0     0     0
{
"0x1DB":[16921,52650,0,0,0,0,0,0,0,0,1],
"readPin":[65535,1189,0,0,0,0,0,0,0,0,0],
"blinkLed":[65535,830,0,0,0,0,0,0,0,0,0]
}

HelloSchedulerWithProfiler

The HelloSchedulerWithProfiler.ino sketch implements the same thing as HelloCoroutineWithProfiler using 2 techniques to handle more than a handful of coroutines:

  • use LogBinProfiler::createProfilers() to automatically create the profilers on the heap and assign them to all coroutines
  • use CoroutineScheduler::loopWithProfiler() method instead of the CoroutineScheduler::loop() method.
#include <AceRoutine.h>
using namespace ace_routine;

const int PIN = 2;
const int LED = LED_BUILTIN;
const int LED_ON = HIGH;
const int LED_OFF = LOW;

COROUTINE(blinkLed) {
  COROUTINE_LOOP() {
    digitalWrite(LED, LED_ON);
    COROUTINE_DELAY(100);
    digitalWrite(LED, LED_OFF);
    COROUTINE_DELAY(500);
  }
}

COROUTINE(printHelloWorld) {
  COROUTINE_LOOP() {
    Serial.print(F("Hello, "));
    Serial.flush();
    COROUTINE_DELAY(1000);
    Serial.println(F("World"));
    COROUTINE_DELAY(4000);
  }
}

COROUTINE(printProfiling) {
  COROUTINE_LOOP() {
    LogBinTableRenderer::printTo(
        Serial, 3 /*startBin*/, 14 /*endBin*/, false /*clear*/);
    LogBinJsonRenderer::printTo(
        Serial, 3 /*startBin*/, 14 /*endBin*/);

    COROUTINE_DELAY(5000);
  }
}

void setup() {
  delay(1000);
  Serial.begin(115200);
  while (!Serial); // Leonardo/Micro

  pinMode(LED, OUTPUT);
  pinMode(PIN, INPUT);

  // Coroutine names can be either C-string or F-string.
  blinkLed.setName("blinkLed");
  readPin.setName(F("readPin"));

  // Create profilers on the heap and attach them to all coroutines.
  LogBinProfiler::createProfilers();

  CoroutineScheduler::setup();
}

void loop() {
  CoroutineScheduler::loopWithProfiler();
}

The printProfiling coroutine will print the same information as before every 5 seconds:

name         <16us <32us <64us<128us<256us<512us  <1ms  <2ms  <4ms  <8ms    >>
0x1DB        16921 52650     0     0     0     0     0     0     0     0     1
readPin      65535  1189     0     0     0     0     0     0     0     0     0
blinkLed     65535   830     0     0     0     0     0     0     0     0     0
{
"0x1DB":[16921,52650,0,0,0,0,0,0,0,0,1],
"readPin":[65535,1189,0,0,0,0,0,0,0,0,0],
"blinkLed":[65535,830,0,0,0,0,0,0,0,0,0]
}

Installation

The latest stable release is available in the Arduino IDE Library Manager. Two libraries need to be installed as of v1.5.0:

  • Search for "AceRoutine". Click Install.
  • It should automatically install (or prompt you to install) the "AceCommon" library.

The development version can be installed by cloning the following git repos:

You can copy these directories to the ./libraries directory used by the Arduino IDE. (You should see 2 directories, named ./libraries/AceRoutine and ./libraries/AceCommon). Or you can create symlinks from /.libraries to these directories.

The develop branch contains the latest working version. The master branch contains the stable release.

Source Code

The source files are organized as follows:

  • src/AceRoutine.h - main header file
  • src/ace_routine/ - implementation files
  • src/ace_routine/testing/ - internal testing files
  • tests/ - unit tests which depend on AUnit
  • examples/ - example programs

Documentation

Examples

The following programs are provided under the examples directory:

Comparisons to Other Multitasking Libraries

There are several interesting and useful multithreading libraries for Arduino. I'll divide the libraries in to 2 camps:

  • tasks
  • threads or coroutines

Task Managers

Task managers run a set of tasks. They do not provide a way to resume execution after yield() or delay().

Threads or Coroutines

In order of increasing complexity, here are some libraries that provide broader abstraction of threads or coroutines:

  • Littlebits coroutines
    • Implemented using Duff's Device which means that nested switch statements don't work.
    • The scheduler has a fixed queue size.
    • The context structure is exposed.
  • Arduino-Scheduler
    • Overrides the system's yield() for a seamless experience.
    • Uses setjmp() and longjmp().
    • Provides an independent stack to each coroutine whose size is configurable at runtime (defaults to 128 for AVR, 1024 for 32-bit processors).
    • ESP8266 or ESP32 not supported (or at least I did not see it).
  • Cosa framework
    • A full-featured, alternative development environment using the Arduino IDE, but not compatible with the Arduino API or libraries.
    • Installs as a separate "core" using the Board Manager.
    • Includes various ways of multi-tasking (Events, ProtoThreads, Threads, Coroutines).
    • The <ProtoThread.h> library in the Cosa framework uses basically the same technique as this AceRoutine library.

Comparing AceRoutine to Other Libraries

The AceRoutine library falls in the "Threads or Coroutines" camp. The inspiration for this library came from ProtoThreads and Coroutines in C where an incredibly brilliant and ugly technique called Duff's Device is used to perform labeled goto statements inside the "coroutines" to resume execution from the point of the last yield() or delay(). It occurred to me that I could make the code a lot cleaner and easier to use in a number of ways:

  • Instead of using Duff's Device, I could use the GCC language extension called the computed goto. I would lose ANSI C compatbility, but all of the Arduino platforms (AVR, Teensy, ESP8266, ESP32) use the GCC compiler and the Arduino software already relies on GCC-specific features (e.g. flash strings using PROGMEM attribute). In return, switch statements would work inside the coroutines, which wasn't possible using the Duff's Device.
  • Each "coroutine" needs to keep some small number of context variables. In the C language, this needs to be passed around using a struct. It occurred to me that in C++, we could make the context variables almost disappear by making "coroutine" an instance of a class and moving the context variables into the member variables.
  • I could use C-processor macros similar to the ones used in AUnit to hide much of the boilerplate code and complexity from the user

I looked around to see if there already was a library that implemented these ideas and I couldn't find one. However, after writing most of this library, I discovered that my implementation was very close to the <ProtoThread.h> module in the Cosa framework. It was eerie to see how similar the 2 implementations had turned out at the lower level. I think the AceRoutine library has a couple of advantages:

  • it provides additional macros (i.e. COROUTINE() and EXTERN_COROUTINE()) to eliminate boilerplate code, and
  • it is a standalone Arduino library that does not depend on a larger framework.

Resource Consumption

Static Memory

All objects are statically allocated (i.e. not heap or stack).

On 8-bit processors (AVR Nano, Uno, etc):

sizeof(Coroutine): 16
sizeof(CoroutineScheduler): 2
sizeof(Channel<int>): 5
sizeof(LogBinProfiler): 66
sizeof(LogBinTableRenderer): 1
sizeof(LogBinJsonRenderer): 1

On 32-bit processors (e.g. Teensy ARM, ESP8266, ESP32):

sizeof(Coroutine): 28
sizeof(CoroutineScheduler): 4
sizeof(Channel<int>): 12
sizeof(LogBinProfiler): 68
sizeof(LogBinTableRenderer): 1
sizeof(LogBinJsonRenderer): 1

The CoroutineScheduler consumes only 2 bytes (8-bit processors) or 4 bytes (32-bit processors) of static memory no matter how many coroutines are created. That's because it depends on a singly-linked list whose pointers live on the Coroutine object, not in the CoroutineScheduler. But using the CoroutineScheduler::loop() instead of calling Coroutine::runCoroutine() directly increases flash memory usage by 70-100 bytes.

The Channel object requires 2 copies of the parameterized <T> type so its size is equal to 1 + 2 * sizeof(T), rounded to the nearest memory alignment boundary (i.e. a total of 12 bytes for a 32-bit processor).

Flash Memory

The examples/MemoryBenchmark program gathers flash and memory consumption numbers for various boards (AVR, ESP8266, ESP32, etc) for a handful of AceRoutine features. Here are some highlights:

Arduino Nano (8-bits)

+--------------------------------------------------------------------+
| functionality                         |  flash/  ram |       delta |
|---------------------------------------+--------------+-------------|
| Baseline                              |   1616/  186 |     0/    0 |
|---------------------------------------+--------------+-------------|
| One Delay Function                    |   1664/  188 |    48/    2 |
| Two Delay Functions                   |   1726/  190 |   110/    4 |
|---------------------------------------+--------------+-------------|
| One Coroutine (millis)                |   1804/  212 |   188/   26 |
| Two Coroutines (millis)               |   1998/  236 |   382/   50 |
|---------------------------------------+--------------+-------------|
| One Coroutine (micros)                |   1776/  212 |   160/   26 |
| Two Coroutines (micros)               |   1942/  236 |   326/   50 |
|---------------------------------------+--------------+-------------|
| One Coroutine (seconds)               |   1904/  212 |   288/   26 |
| Two Coroutines (seconds)              |   2130/  236 |   514/   50 |
|---------------------------------------+--------------+-------------|
| One Coroutine, Profiler               |   1874/  212 |   258/   26 |
| Two Coroutines, Profiler              |   2132/  236 |   516/   50 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (millis)     |   1928/  214 |   312/   28 |
| Scheduler, Two Coroutines (millis)    |   2114/  238 |   498/   52 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (micros)     |   1900/  214 |   284/   28 |
| Scheduler, Two Coroutines (micros)    |   2058/  238 |   442/   52 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (seconds)    |   2028/  214 |   412/   28 |
| Scheduler, Two Coroutines (seconds)   |   2246/  238 |   630/   52 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (setup)      |   1978/  214 |   362/   28 |
| Scheduler, Two Coroutines (setup)     |   2264/  238 |   648/   52 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (man setup)  |   1956/  214 |   340/   28 |
| Scheduler, Two Coroutines (man setup) |   2250/  238 |   634/   52 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine, Profiler    |   1992/  214 |   376/   28 |
| Scheduler, Two Coroutines, Profiler   |   2178/  238 |   562/   52 |
|---------------------------------------+--------------+-------------|
| Scheduler, LogBinProfiler             |   2112/  286 |   496/  100 |
| Scheduler, LogBinTableRenderer        |   3514/  304 |  1898/  118 |
| Scheduler, LogBinJsonRenderer         |   3034/  308 |  1418/  122 |
|---------------------------------------+--------------+-------------|
| Blink Function                        |   1948/  189 |   332/    3 |
| Blink Coroutine                       |   2118/  212 |   502/   26 |
+--------------------------------------------------------------------+

ESP8266 (32-bits)

+--------------------------------------------------------------------+
| functionality                         |  flash/  ram |       delta |
|---------------------------------------+--------------+-------------|
| Baseline                              | 264981/27984 |     0/    0 |
|---------------------------------------+--------------+-------------|
| One Delay Function                    | 265045/27992 |    64/    8 |
| Two Delay Functions                   | 265109/27992 |   128/    8 |
|---------------------------------------+--------------+-------------|
| One Coroutine (millis)                | 265177/28028 |   196/   44 |
| Two Coroutines (millis)               | 265337/28060 |   356/   76 |
|---------------------------------------+--------------+-------------|
| One Coroutine (micros)                | 265209/28028 |   228/   44 |
| Two Coroutines (micros)               | 265369/28060 |   388/   76 |
|---------------------------------------+--------------+-------------|
| One Coroutine (seconds)               | 265209/28028 |   228/   44 |
| Two Coroutines (seconds)              | 265385/28060 |   404/   76 |
|---------------------------------------+--------------+-------------|
| One Coroutine, Profiler               | 265257/28028 |   276/   44 |
| Two Coroutines, Profiler              | 265433/28060 |   452/   76 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (millis)     | 265241/28036 |   260/   52 |
| Scheduler, Two Coroutines (millis)    | 265385/28060 |   404/   76 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (micros)     | 265257/28036 |   276/   52 |
| Scheduler, Two Coroutines (micros)    | 265401/28060 |   420/   76 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (seconds)    | 265257/28036 |   276/   52 |
| Scheduler, Two Coroutines (seconds)   | 265417/28060 |   436/   76 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (setup)      | 265273/28036 |   292/   52 |
| Scheduler, Two Coroutines (setup)     | 265433/28060 |   452/   76 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine (man setup)  | 265257/28036 |   276/   52 |
| Scheduler, Two Coroutines (man setup) | 265433/28060 |   452/   76 |
|---------------------------------------+--------------+-------------|
| Scheduler, One Coroutine, Profiler    | 265321/28036 |   340/   52 |
| Scheduler, Two Coroutines, Profiler   | 265449/28060 |   468/   76 |
|---------------------------------------+--------------+-------------|
| Scheduler, LogBinProfiler             | 265465/28100 |   484/  116 |
| Scheduler, LogBinTableRenderer        | 267381/28100 |  2400/  116 |
| Scheduler, LogBinJsonRenderer         | 266789/28104 |  1808/  120 |
|---------------------------------------+--------------+-------------|
| Blink Function                        | 265669/28064 |   688/   80 |
| Blink Coroutine                       | 265801/28100 |   820/  116 |
+--------------------------------------------------------------------+

Comparing Blink Function and Blink Coroutine is probably the most fair comparison, because they implement the exact same functionality. The code is given in Comparison To NonBlocking Function. The Blink Function implements the asymmetric blink (HIGH and LOW having different durations) functionality using a simple, non-blocking function with an internal prevMillis static variable. The Blink Coroutine implements the same logic using a Coroutine. The Coroutine version is far more readable and maintainable, with only about 220 additional bytes of flash on AVR, and 130 bytes on an ESP8266. In many situations, the increase in flash memory size may be worth paying to get easier code maintenance.

CPU

See examples/AutoBenchmark. Here are 2 samples:

Arduino Nano:

+---------------------------------+--------+-------------+--------+
| Functionality                   |  iters | micros/iter |   diff |
|---------------------------------+--------+-------------+--------|
| EmptyLoop                       |  10000 |       1.700 |  0.000 |
|---------------------------------+--------+-------------+--------|
| DirectScheduling                |  10000 |       2.900 |  1.200 |
| DirectSchedulingWithProfiler    |  10000 |       5.700 |  4.000 |
|---------------------------------+--------+-------------+--------|
| CoroutineScheduling             |  10000 |       7.100 |  5.400 |
| CoroutineSchedulingWithProfiler |  10000 |       9.300 |  7.600 |
+---------------------------------+--------+-------------+--------+

ESP8266:

+---------------------------------+--------+-------------+--------+
| Functionality                   |  iters | micros/iter |   diff |
|---------------------------------+--------+-------------+--------|
| EmptyLoop                       |  10000 |       0.200 |  0.000 |
|---------------------------------+--------+-------------+--------|
| DirectScheduling                |  10000 |       0.500 |  0.300 |
| DirectSchedulingWithProfiler    |  10000 |       0.800 |  0.600 |
|---------------------------------+--------+-------------+--------|
| CoroutineScheduling             |  10000 |       0.900 |  0.700 |
| CoroutineSchedulingWithProfiler |  10000 |       1.100 |  0.900 |
+---------------------------------+--------+-------------+--------+

System Requirements

Hardware

Tier 1: Fully Supported

These boards are tested on each release:

  • Arduino Nano (16 MHz ATmega328P)
  • SparkFun Pro Micro (16 MHz ATmega32U4)
  • STM32 Blue Pill (STM32F103C8, 72 MHz ARM Cortex-M3)
  • NodeMCU 1.0 (ESP-12E module, 80 MHz ESP8266)
  • WeMos D1 Mini (ESP-12E module, 80 MHz ESP8266)
  • ESP32 dev board (ESP-WROOM-32 module, 240 MHz dual core Tensilica LX6)
  • Teensy 3.2 (96 MHz ARM Cortex-M4)

Tier 2: Should work

These boards should work, but they are not tested frequently by me, or I don't own the specific hardware so they were tested by a community member:

Tier 3: May work, but not supported

  • SAMD21 M0 Mini (48 MHz ARM Cortex-M0+)
    • Arduino-branded SAMD21 boards use the ArduinoCore-API, so are explicitly blacklisted. See below.
    • Other 3rd party SAMD21 boards may work using the SparkFun SAMD core.
    • However, as of SparkFun SAMD Core v1.8.6 and Arduino IDE 1.8.19, I can no longer upload binaries to these 3rd party boards due to errors.
    • Therefore, third party SAMD21 boards are now in this new Tier 3 category.
    • This library may work on these boards, but I can no longer support them.

Tier Blacklisted

The following boards are not supported and are explicitly blacklisted to allow the compiler to print useful error messages instead of hundreds of lines of compiler errors:

Tool Chain

This library was developed and tested using:

This library is not compatible with:

It should work with PlatformIO but I have not tested it.

The library works on Linux or MacOS (using both g++ and clang++ compilers) using the EpoxyDuino emulation layer.

Operating System

I use Ubuntu 20.04 for the vast majority of my development. I expect that the library will work fine under MacOS and Windows, but I have not explicitly tested them.

License

MIT License

Feedback and Support

If you have any questions, comments, or feature requests for this library, please use the GitHub Discussions for this project. If you have bug reports, please file a ticket in GitHub Issues. Feature requests should go into Discussions first because they often have alternative solutions which are useful to remain visible, instead of disappearing from the default view of the Issue tracker after the ticket is closed.

Please refrain from emailing me directly unless the content is sensitive. The problem with email is that I cannot reference the email conversation when other people ask similar questions later.

Authors

Created by Brian T. Park ([email protected]).

aceroutine's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

aceroutine's Issues

restarting a Coroutine after it has finished

So im using AceRoutine for an arduino project and theres some things that im confused about. ill try to explain what im trying to do but if you dont understand please let me know. Lets say i have an Alarm Coroutine. that coroutine flashes a light on and off 10 times.

COROUTINE(Alarm){
COROUTINE_BEGIN();
static int Alarmi1 = 0;
for (Alarmi1 = 0; Alarmi1 < 10; Alarmi1++)
{
digitalWrite(A3, 1);
COROUTINE_DELAY(50);
digitalWrite(A3, 0);
COROUTINE_DELAY(50);
}
COROUTINE_END();
}

So what i want is a way for me to be able to call Alarm.RunOnce(). and it runs the coroutine Once and flashes the light 10 times.
the problem is that it just Turns the light on and stops when i try Alarm.runCorutine(). now i believe the function runCoroutine is what is constantly checking if it should start back up and resume exicution so you need to have that function in the loop. but the problem is when i do that it blinks 10 times and it stops. and whenever i call runCoroutine again nothing happens. it just gets locked down. any help would be apprecheated if you have discord and would like to talk there my discord is BiffBish#9043

Microsecond Timing?

I find myself needing sub-millisecond timings.
Presumably COROUTINE_AWAIT(microSeconds() > somestaticvariable)
will work, however, is there a better way, or is it worth making a new
convenience macro?

CoRoutine will stop work when you make type conversion in it...

To replicate problem include into your co-routine conversion from unsigned long into String:
Btw., i did not try more combinations such like long into String and so on..

Remark: I have run the same code without co-routines , just in a standard loop, the conversion part,
and there are no errors or warnings - it did exact what it supposed to.. :) Converted :P

Remark2: Tested on Teensy 3.6 under Arduino IDE latest revision.

`
//This is partially pseudo-code, with exception of problematic place...

// Global Variable
unsigned long VariableX = 0;

// Pseudo code for illustration only
COROUTINE(Loop1) {
COROUTINE_LOOP() {

Math for VariableX happening here..

}
}

COROUTINE(Loop2) { // Pseudo code
COROUTINE_LOOP() {

// This code here hangs co-routine at conversion from unsigned long into String..
String SomeVar = VariableX; // conversion here from unsigned long to String
Serial.println(SomeVar);

COROUTINE_DELAY(10);

}
}

/* please do not go into discussion of why i needed this.
It is a must for me. This code represent the problem, but not why i have used it on the first place, which is out of scope of this Bug report */

`

`reset()` causing coroutine to break somehow

reset() either seems to be breaking coroutines its called on, or I am not understanding how it should be used.

I am building a device that needs to play short melodies, where a melody is a sequence of Arduino tone() calls with a delay in between. I want to use AceRoutine to implement a non-blocking sound manager which can use e.g. COROUTINE_DELAY in between calls to tone(). However, sometimes a new melody needs to be played before the previous melody is finished, in which case I want the sound manager coroutine to notice the "interruption" (not a real ISR, just the concept) and immediately switch to the new melody. I expected that I could use Coroutine::reset to achieve this, because it would force the coroutine to go back to the coroutine's start and start processing the new melody. However, I'm finding that calling reset() on the sound manager coroutine causes it to stop running somehow. After reset() is called the first time, the sound manager coroutine is also no longer printed from CoroutineScheduler::list.

Things I tried to fix it:

  • After calling reset(), also call setupCoroutine() again, in the theory that maybe I needed to reinitialize the coroutine after resetting it. Weirdly, this did cause the sound manager coroutine to execute once... but never again, and also my other coroutines stopped executing too. I assume this is not the proper way to fix this.
  • Remove the call to reset(). This did "fix" the immediate issue, however it means that the sound manager coroutine will need to manually check for the interruption condition every time it resumes from a COROUTINE_DELAY, which is a lot of ugly unnecessary work. However, this is what I'm doing now to unblock myself until I can find out whether reset() has a bug or I'm just not using it right.

Here is the simplest code I could get that shows the issue.

#include <AceRoutine.h>

using namespace ace_routine;

const int SOUND_NONE = 0;
const int SOUND_BEEP = 1;

// This is the "sound manager" coroutine.
class SoundRoutine : public Coroutine {
  public:
    int runCoroutine() override;
    void playSound(int sound);
  private:
    int currentSound;
};

void SoundRoutine::playSound(int sound) {
  this->currentSound = sound;
  // Comment out this reset() call to "fix" the problem... except then I'd have to
  // implement checking-for-changed-currentSound myself.
  this->reset();
}

int SoundRoutine::runCoroutine() {
  COROUTINE_LOOP() {
    switch (this->currentSound) {
      case SOUND_NONE:
        Serial.println("<silence>");
        break;
      case SOUND_BEEP:
        // TODO: tone() will go here, interspersed with some COROUTINE_DELAYs. Ideally I do not
        // want to have to check for currentSound being changed manually after every COROUTINE_DELAY,
        // so I want to call reset() in the playSound() function to force the coroutine to return to
        // the start of the loop.
        Serial.println("First BEEP!");
        COROUTINE_DELAY(500);
        // Without reset(), I'd need to check whether currentSound is different here.
        Serial.println("Second BEEP!");
        break;
      default:
        Serial.println("Unknown sound!");
    }
    this->currentSound = SOUND_NONE;
    COROUTINE_AWAIT(this->currentSound != SOUND_NONE);
  }
}

SoundRoutine sound;

// This coroutine emulates some other piece of code that needs to be able to trigger a sound.
COROUTINE(trigger) {
  COROUTINE_LOOP() {
    COROUTINE_DELAY(5000);
    Serial.println("Triggering...");
    CoroutineScheduler::list(Serial);
    sound.playSound(SOUND_BEEP);
  }
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  sound.setupCoroutine("sound");
  CoroutineScheduler::setup();
}

void loop() {
  CoroutineScheduler::loop();
}

Resetting a coroutine

A couple questions:

Is it possible to reset a coroutine so that it starts from the beginning?
Is it possible to suspend all coroutines that are currently running, and then resume only the coroutines that were paused?

Very nice work with this library!

Jeff

Implementing manual (Class) based coroutines with .h and .cpp files

Hi,

Right off the bat I would like to sincerely thank you for the awesome work you've done and this and your other libraries. Both the code quality and effort invested is truly commendable, and has certainly not gone unnoticed or unappreciated.

I'd like to ask if there is is any way in which the Sound Manager example could be implemented in a separate .h and .cpp file.
I would continuously get the following error undefined reference to `vtable for SoundManager'

As such I eventually settled for implementing everything in a single separate SoundManager.h file which compiles without errors.

delayStartMillis should be a uint32_t instead of uint16_t

Currently, mDelayStartMillis and mDelayDurationMillis are both uint16_t to save memory. However, I think that mDelayStartMillis should be changed to a uint32_t to prevent overflows if COROUTINE_DELAY() is very close to UINT16_MAX.

The problem is not the overflow of the mDelayStartMillis, because the time delta expression ((uint16_t) millis() - mDelayStartMillis) is correct even if millis() overflows the lower 16-bits, as long as millis() is within UINT16_MAX of the original mDelayStartMillis. The problem occurs if mDelayDurationMillis is very close to UINT16_MAX. This means that the check for the expiration of the delay must occur more frequently than the small gap between mDelayStartMillis and (UINT16_MAX + 1). The worst case is if mDelayStartMillis is within 1 or 2 millisecond of UINT16_MAX. Then window of opportunity for successive calls to the coroutine (or the scheduler) to correctly determine the expiration of the delay is only 1-2 milliseconds. In real life, it is likely that the total execution time of the other coroutines in the system could exceed 1-2 milliis, in which case the COROUTINE_DELAY() would fail and the coroutine executing that statement would fail to run again.

The solution is to make mDelayStartMillis a uint32_t, while keeping mDelayDurationMillis as a uint16_t. As long as the entire time-delta calculation is performed using 32 bits, the algorithm is not susceptible to the small window problem. The tradeoff is that the 32-bit arithmetic is much slower on 8-bit processors, and we use 2 more bytes for each coroutine.

More a suggestion or offer ... of incorporating my local changes of 1.4.3 into 1.5.1

I've been using AceRoutine 1.4.3 for a while (and made a few local tweaks), and considering the upgrade to 1.5.1.
TBH I haven't checked your latest code, but reading your change descriptions, I expect my tweaks are not fully compatible (which is my problem, not yours).
My local changes were done primarily to save ROM & RAM, keep track of status changes, and timings, for debugging purposes.
Anyway ... are you interested in me adding my tweaks as a Pull Request, once I've done the work? (Yes, I should have done this a year ago, but ...)
FWIW, squashing my changes and looking at them, they involve:

  • add void Coroutine_##name::printName(Print* pPrinter) const override;
  • '#define ACE_RUN_TICKS 3' for timing in mS, or 6 for uS, for timing displays.
  • store line number when changing status; nice to know WHO made the status change!
  • add class ICoroutineVisitor, to visit all existing CoRoutines (this is handy for displaying stats)
  • split status Yielding into Yielding and Cold (never started)
  • add status Awaiting (it's ready to run but not been scheduled)
  • been 'creative' with status values; using a variety of compile-time options of:
    • single characters, such as 'Y' for Yielding; makes display/print of human readable status easier
    • 32 bit values hammered into sz 3-letter Words such as 'Yld' and 'Sus'
    • Use F() strings to display full status name from single byte status e.g. 'Terminated' or 'Trmntd' (choice via compile time option)
  • keep track of running time without yield or delay
  • keep description (as F() string) of reason (or time delay period) for each status change

Regards

Bryan 'brewmanz' White

PS I could do them as multiple Pull Requests, by feature

HelloCoroutine on Platformio on Digispark (Attiny85) fails compile

I've tried compiling the HelloCoroutine code on Platformio for the Digispark board, which is attiny85 based.
I get the following compile errors

Demo repo is here: [email protected]:mkarliner/AceTest.git

In file included from .pio/libdeps/digispark-tiny/AceCommon/src/AceCommon.h:55:0,
                 from .pio/libdeps/digispark-tiny/AceRoutine/src/ace_routine/Coroutine.h:30,
                 from .pio/libdeps/digispark-tiny/AceRoutine/src/AceRoutine.h:49,
                 from src/main.cpp:8:
.pio/libdeps/digispark-tiny/AceCommon/src/print_str/PrintStr.h:130:10: error: 'void ace_common::PrintStrBase::flush()' marked 'override', but does not override
     void flush() override {
          ^~~~~
Compiling .pio/build/digispark-tiny/libfea/AceRoutine/ace_routine/Coroutine.cpp.o
*** [.pio/build/digispark-tiny/src/main.cpp.o] Error 1
In file included from .pio/libdeps/digispark-tiny/AceCommon/src/AceCommon.h:55:0,
                 from .pio/libdeps/digispark-tiny/AceRoutine/src/ace_routine/Coroutine.h:30,
                 from .pio/libdeps/digispark-tiny/AceRoutine/src/ace_routine/Coroutine.cpp:25:
.pio/libdeps/digispark-tiny/AceCommon/src/print_str/PrintStr.h:130:10: error: 'void ace_common::PrintStrBase::flush()' marked 'override', but does not override
     void flush() override {
          ^~~~~
*** [.pio/build/digispark-tiny/libfea/AceRoutine/ace_routine/Coroutine.cpp.o] Error 1

Examples don't set LED pinMode

In examples such as HelloCoroutine, the led does not flash as its pinMode has not been set to
OUTPUT. At least, that is the case on a clone ATMega2650.

Separately, I'm probably being very stupid, but in examples such as HelloCoroutine, I would have
expected say, printHello to be run again when it has completed rather than as a one shot.
Am I missing something?

Setup() lifecycle method in Manual Routine method.

Hi bxparks,

To start, thanks for your work, you are amazing. Your lib is a good answer to a lot of problems I was manually solving before.
If I were to design it by hand, I would however add a setup() method that can be overwritten to initialize resources while starting the CoroutineSheduler (with CoroutineSheduler ::setup()).
If I'm not mistaken, this could be done here :

 void setupScheduler() {
     // start of mod
    for (T_COROUTINE** p = T_COROUTINE::getRoot(); (*p) != nullptr;
          p = (*p)->getNext()) {
        (*p)->setupCoroutine();
      }
    // end of mod
      mCurrent = T_COROUTINE::getRoot();
}

This would on the other hand require the addition of the empty lifecycle method setupCoroutine() in the Coroutine class in order for it to be overwritten if needed.

Tho we can emulate the behavior I propose by inserting code before a loop in the coroutine body, I think this would better separate concerns in the code.
This could be useful to open pins or other resources without sacrificing readability.

Let me know what you think, I can submit a pull request if you think that this would be a good thing :)

Stack overflow from CoroutineScheduler::list() after suspend() -> resume()

The following code causes a stack overflow within CoroutineScheduler::list() using AceRoutine 1.1.0 and AceCommon 1.1.1 when running on a ESP8266 (NodeMCU 12e module).

#include <AceRoutine.h>

using namespace ace_routine;

COROUTINE(hello) {
  static int i = 0;
  COROUTINE_LOOP() {
    Serial.printf("Hello coroutine [%d]\r\n", i);
    i += 1;
    COROUTINE_DELAY(1000);
  }
}

void setup() {
  Serial.begin(115200);
  CoroutineScheduler::setup();
  hello.suspend();
  hello.resume();
  CoroutineScheduler::list(Serial);
}

void loop() {
  CoroutineScheduler::loop();
}

I took a quick look into the loop() code and based on what its doing its possible that the combination of suspend() -> resume() is creating a circular loop inside the CoroutineScheduler's linked list.

for loops inside COROUTINE don't seem to work

I have a coroutine to read current off a sensor:

int sensorValue = 0;
const int avgSamples = 10;

analogInPin = A0;

COROUTINE(readCurrent) {
  COROUTINE_LOOP() {
    for (int i = 0; i < avgSamples; i++)
    {
      sensorValue += analogRead(analogInPin);
      COROUTINE_YIELD();
    }

    sensorValue /= avgSamples;
    Serial.println(sensorValue);
  }
}

It never prints anything. The loop simply executes forever (but does yield to other coroutines).

Similarly if I do the following with a delay instead:

int sensorValue = 0;
const int avgSamples = 10;

analogInPin = A0;

COROUTINE(readCurrent) {
  COROUTINE_LOOP() {
    for (int i = 0; i < avgSamples; i++)
    {
      sensorValue += analogRead(analogInPin);
      COROUTINE_DELAY(2);
    }

    sensorValue /= avgSamples;
    Serial.println(sensorValue);
  }
}

values do get printed, however, it seems to be just a single sample instead of the aggregated values.

Proposal: remove the 2-argument COROUTINE(class, name) macro

In the next version of AceRoutine, I am thinking about removing the 2-argument version of COROUTINE(class, name) macro. In the README.md, this is called "Custom Coroutines", in an attempt to distinguish it from "Manual Coroutines". Personally, I have found that the "Manual Coroutine" is far more useful, and I have never actually used the "Custom Coroutine". (See explanation of the 2 types below.) I actually find it hard to remember the difference in terminology, and I also find it hard to remember exactly how the "Custom Coroutine" feature is implemented. Removing this feature will make the code easier to maintain in the long run.

The 2-argument COROUTINE() macro allows the ace_routine::Coroutine subclass to be defined, then the body of the runRoutine() method will be defined by the COROUTINE() macro, like this:

class CustomCoroutine : public Coroutine {
  public:
    void enable(bool isEnabled) { enabled = isEnabled; }

    // the runCoroutine() method will be defined by the COROUTINE() macro

  protected:
    bool enabled = 0;
};

COROUTINE(CustomCoroutine, blinkSlow) {
  COROUTINE_LOOP() {
    ...
  }
}

The feature seemed like a natural generalization of the 1-argument COROUTINE() macro. But in practice, I have never found it useful because this does not allow a custom constructor to be defined. The constructor is defined by the COROUTINE() macro, which cannot allow additional parameters to be injected into the CustomCoroutine class through the constructor.

Instead, the "Manual Coroutine" is far more useful, and I have used it numerous times. The end-user has full control over the constructor, to inject additional dependencies, and it is easier to think about the manual coroutine as a "task" object that is handed over to the scheduler (via the Coroutine::setupCoroutine() method).

class ManualCoroutine : public Coroutine {
  public:
    // Inject external dependencies into the constructor.
    ManualCoroutine(Params, ..., Objects, ...) {
      ...
    }

  private:
    int runCoroutine() override {
      COROUTINE_BEGIN();
      // insert coroutine code here
      COROUTINE_END();
    }
};

// Create an instance of the coroutine, with dependencies.
ManualCoroutine manualRoutine(params, ..., objects, ...);

void setup() {
  ...
  manualRoutine.setupCoroutine("manualRoutine");
  CoroutineScheduler::setup();
}

All "Custom Coroutines" can be rewritten as a "Manual Coroutine", so I think the removal of this feature will not be traumatic.

Please reply to this thread if you have any thoughts.

COROUTINE_DELAY_SECONDS & COROUTINE_DELAY_MICROS not working

I use it like this:

COROUTINE(ledLoop) 
{
  COROUTINE_LOOP() 
  {  
      static uint16_t i3b;
      for(i3b=STARS_START_LED; i3b<NUM_LEDS_TOTAL; i3b++) 
      {
          if (ledEffect_stars != 1) break;
          leds[i3b] = CRGBW(redStars, greenStars, blueStars, whiteStars);
          // not working: 
          COROUTINE_DELAY_SECONDS(1);
      }   
  }
}

All code is working fine but there is no delay happening. (using different ESPs with PlatformIO)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.