GithubHelp home page GithubHelp logo

ssilverman / teensydmx Goto Github PK

View Code? Open in Web Editor NEW
95.0 14.0 6.0 887 KB

A full-featured DMX library for Teensy 3, Teensy LC, and Teensy 4. "Programmable DMX and arbitrary USB serial device emulation."

License: BSD 3-Clause Clear License

C++ 100.00%
teensy dmx dmx512 rdm rdm-responder teensy3 teensy-lc teensy32 teensy35 teensy36

teensydmx's Introduction

Buy Me A Coffee donate button

Readme for TeensyDMX v4.3.0-snapshot

This is a full-featured library for receiving and transmitting DMX on Teensy 3, Teensy LC, and Teensy 4. It follows the ANSI E1.11 DMX512-A specification.

Table of contents

  1. Features
    1. Receiver timing limitations and RX line monitoring
    2. Transmitter timing limitations
  2. The TODO list
  3. How to use
    1. Examples
    2. Synchronous vs. asynchronous operation
  4. DMX receive
    1. Code example
    2. Retrieving 16-bit values
    3. Error counts and disconnection
      1. The truth about connection detection
      2. Keeping short packets
    4. Packet statistics
    5. Error statistics
    6. Synchronous operation by using custom responders
      1. Responding
  5. DMX transmit
    1. Code example
    2. Packet size
    3. Transmission rate
    4. Synchronous operation by pausing and resuming
    5. Choosing BREAK and MAB times
      1. Specific BREAK/MAB times
        1. A note on BREAK timing
        2. A note on MAB timing
      2. BREAK/MAB times using serial parameters
    6. Inter-slot MARK time
    7. MBB time
    8. Error handling in the API
  6. Technical notes
    1. Simultaneous transmit and receive
    2. Transmission rate
    3. Transmit/receive enable pins
    4. Thread safety
    5. Dynamic memory allocation failures
    6. Hardware connection
    7. Receiver and driving the TX pin
    8. Potential PIT timer conflicts
  7. Code style
  8. References
  9. Acknowledgements

Features

Some notable features of this library:

  1. Teensy's default serial buffer isn't used; the data goes directly to/from the DMX buffers from/to the UART ISRs. In other words, the library is asynchronous and runs independently; all you need to worry about is setting and getting channel data.
  2. Simple API: After setup, there's only two read calls (readPacket and get) and two forms of one write call (set for single and multiple channels).
  3. The library properly handles DMX packets containing less than 513 slots.
  4. The transmitter refresh rate can be changed to something less than "maximum rate".
  5. The transmitter can be paused and resumed to allow for packets that must be adjacent to other packets. In other words, the asynchronous transmitter can be used synchronously. For example, System Information Packets (SIP) require this. See Annex D5 of ANSI E1.11.
  6. The transmitter timing parameters can be specified: BREAK, MAB, inter-slot MARK time, and MBB.
  7. The receiver checks for timeouts according to the DMX specification. It knows of the concept of being disconnected from a DMX transmitter when timeouts or bad BREAKs are encountered in the data stream.
  8. Packet and error statistics are available. These can be used to detect protocol problems, including timeouts, framing errors and bad BREAKs, short packets (those less than 1196us), and long packets (those that exceed 513 bytes).
  9. The receiver can be used synchronously through the use of the Responder API. Alternate start codes can not only be handled, for example, for Text Packets or System Information Packets (SIP), but responses can be sent back to the transmitter, for example for RDM.
  10. Functions for handling 16-bit data.

Receiver timing limitations and RX line monitoring

When the RX line is not being monitored, there are limitations in the handling of received DMX frame timing. For BREAK and Mark after BREAK (MAB) times, only the following cases are checked and not accepted as a valid DMX frame start:

  1. BREAK duration < ~44us.
  2. BREAK duration + MAB duration < ~96us.
  3. BREAK < ~88us and MAB ≥ ~44us.

The following case is accepted as a valid frame start, even though it isn't compliant with the DMX specification:

  1. BREAK duration > ~52us and MAB duration < ~44us.

For example, if a BREAK comes in having a duration of 53us and then a MAB comes in having a duration of 43us, their sum is 96us, and so the packet will be accepted. More generally, this will allow BREAKs that are shorter than the minimum required 88us if the MAB is shorter than 44us.

This limitation does not exist if the RX line is monitored. To monitor the line, connect it to a digital I/O-capable pin and call setRXWatchPin with the pin number. The pin cannot be the same as the RX pin.

Transmitter timing limitations

The transmitter uses a UART to control all the output. On the Teensy, the UART runs independently. This means that any specified timings won't necessarily be precise. They will often be accurate to within one or maybe two bit times. An effort has been made, however, to make sure that transmitted timings are at least as long as the requested timings.

For example, if a 100us MAB is requested, the actual MAB may be 102 or 103us. If a 40us inter-slot time is requested, the actual time may be 44us. And so on.

Additionally, certain timings will have a minimum length that can't be shortened, for example the MBB. This is simply due to code execution time interacting with the interrupt and UART subsystems. For example, on a Teensy LC, the minimum MBB is about 119us, even if the requested value is 0us.

The TODO list

These are either in the works or ideas for subsequent versions:

  1. Asynchronous responder data. Currently, the data is sent synchronously inside the UART ISR where responders process the packet.
  2. Better MAB transmit timing, perhaps by somehow synchronizing with the baud rate clock.
  3. Explore much more precise transmitter timings by not using the UART.

How to use

The classes you'll need are in the qindesign::teensydmx namespace: Receiver and Sender.

All class documentation can be found in src/TeensyDMX.h.

Examples

Receiver examples:

  • BasicReceive: A basic receive example
  • Flasher: Change the flash speed of the board LED based on DMX input

Sender examples:

  • BasicSend: A basic send example
  • Chaser: Chases values across all channels
  • SendADC: Sends the value from an ADC over a DMX channel
  • SendTestPackets: Sends test packets (start code 55h)

Examples that show how to utilize synchronous and asynchronous transmission:

  • SIPSenderAsync: Sends SIP packets asynchronously
  • SIPSenderSync: Sends SIP packets synchronously

Examples that show how to use a synchronous packet handler in a receiver:

  • SIPHandler: Understands System Information Packets (SIP) (start code CFh)
  • TextPacketHandler: Understands text packets (start codes 17h and 90h)

Transmitter timing examples:

  • RegenerateDMX: Regenerates received DMX onto a different serial port and with different timings

Other examples:

  • FastLEDController: Demonstrates DMX pixel output using FastLED

A more complex example showing how to behave as a DMX USB Pro Widget is in USBProWidget.

Synchronous vs. asynchronous operation

Both transmission and reception operate asynchronously. This means that there's potentially a continuous stream of data being sent or received in the background.

The transmitter will keep sending the same data until it's changed externally using one of the Sender::set functions. Similarly, Receiver::readPacket and Receiver::get will return only the latest data; if more than a small amount of time elapses between calls, then the data may have been changed more than once.

Both Sender and Receiver have a mode where they can operate synchronously. With Sender, the stream can be paused and resumed, and packets inserted at appropriate spots. Similarly, Receiver can send received packets as they arrive to start code-specific instances of Responder.

DMX receive

Code example

First, create an object using one of the hardware serial ports, different from any serial ports being used for transmission:

namespace teensydmx = ::qindesign::teensydmx;

teensydmx::Receiver dmxRx{Serial1};

Before using the instance, start the serial port internals:

dmxRx.begin();

Using your own buffer whose length is at least len bytes, check for a packet having arrived, and if it has, copy len values starting from channel startChannel:

// For this example, assume buf is at least len bytes long
int read = dmxRx.readPacket(buf, startChannel, len);

read will contain the number of bytes read, -1 if no packet is available, or zero if no values were read.

Note that channel zero contains the start code, a special value, usually zero, that occupies the first byte of the packet. The maximum DMX packet size is 513, but may be smaller, depending on the system.

Each call to readPacket is independent, meaning that if no packet has arrived after a call to this function, subsequent calls will return -1.

Retrieving 16-bit values

Retrieving 16-bit values is easy with one available 16-bit function:

  1. get16Bit for single words.

This works the same as the 8-bit get function, but uses the uint16_t type instead.

Error counts and disconnection

The DMX receiver keeps track of three types of errors:

  1. Packet timeouts.
  2. Framing errors, including bad BREAKs.
  3. Short packets, i.e. those packets that occupy less than 1196us.

The counts can be retrieved via errorStats(). This returns an ErrorStats object containing each of the error counts. These metrics are reset to zero when the receiver is started or restarted.

An associated concept is disconnection. A receiver is considered connected when it is receiving valid DMX packets. When any timeouts occur, or when invalid BREAKs are detected, then the receiver considers itself disconnected. As soon as valid packets reappear, the receiver is once again connected.

To determine whether a receiver is connected, call its connected() function. A callback function, to be notified when this event happens, can also be set using onConnectChange.

The following example sets the built-in LED according to the current connected state. It uses a lambda expression, but a normal function could be used instead.

dmxRx.onConnectChange([](Receiver *r) {
  digitalWriteFast(LED_BUILTIN, r.connected() ? HIGH : LOW);
});

The truth about connection detection

In actual fact, the connection detection only works for most of the cases where timeouts occur or when bad BREAKs happen. The one thing it can't do is detect when a BREAK condition occurs permanently, and, if none of the periodic interrupt timers (PIT) are available, when an IDLE condition occurs permanently. Unless more data comes in, no further UART interrupts will be generated, so it is up to the user's code to detect this case.

The Flasher example contains an example of how to do this. In essence, the readPacket function will return a positive value if the desired data is available in the last packet received. A timer can be reset when valid data is received, and then subsequent code can use the difference between the current time and the timer value to determine if there's been a timeout.

An example that uses elapsedMillis to encapsulate the current time call:

constexpr uint32_t kTimeout = 1000;  // In milliseconds

elapsedMillis lastPacketTimer{0};
uint8_t buf[250];

void loop() {
  int read = dmxRx.readPacket(buf, 0, 217);
  if (read == 217) {  // Comparing to > 0 means that _some_ data was received
    // All the requested data was received
    // Do stuff with it
    lastPacketTimer = 0;
  }

  if (lastPacketTimer < kTimeout) {
    // Do work
  } else {
    // No connection
  }
}

A second technique would be to use the value returned from lastPacketTimestamp() as something that closely approximates the true latest timestamp. For example:

constexpr uint32_t kTimeout = 1000;  // In milliseconds

void loop() {
  if (millis() - dmxRx.lastPacketTimestamp() >= kTimeout) {
    // Respond to the timeout
  }
  // Do work
}

In actuality, a timer could be used to detect this condition, but it was chosen to not do this because it would use up a timer, add one extra timeout-specifying method, and the user code is probably good enough.

In summary, the connected concept here has more to do with line noise and bad timing than it does with a physical connection. Perhaps a future release will rename this API concept or address it with the timer...

Keeping short packets

Short packets are nothing more than valid-looking packets that span a duration less than 1196us. The default is to discard them because they may indicate line noise.

It is possible to keep the data from these packets by enabling the "Keep Short Packets" feature using the setKeepShortPackets function. If these are kept then the PacketStats::isShort variable will indicate whether the associated packet is a short packet.

Recall that the readPacket function can atomically retrieve packet statistics associated with the packet data. If packetStats() is used instead, then there's no guarantee that the values will be associated with the most recent or next packet data retrieval calls.

Note that there is also an isKeepShortPackets function that can be polled for the current state of this feature.

Packet statistics

Packet statistics are tracked and the latest can be retrieved from a PacketStats object returned by packetStats(). Included are these variables:

  1. size: The latest received packet size.
  2. isShort: Whether the last packet was a short packet, a packet having a duration less than 1196us. Keeping these packets can be enabled with the setKeepShortPackets function.
  3. timestamp: The timestamp of the last packet received, in milliseconds. This is set to the time the packet was recognized as a packet and not the end of the last stop bit.
  4. breakPlusMABTime: The sum of the BREAK and MAB times, in microseconds. It's not possible to determine where the BREAK ends and the MAB starts without using another pin to watch the RX line. To set up a connected pin, see setRXWatchPin and the section above on RX line monitoring.
  5. breakToBreakTime: The latest measured BREAK-to-BREAK time, in microseconds. Note that this is not collected at the same time as the other variables and only represents the last known duration. This will be out of sync with the rest of the values in the presence of packet errors.
  6. frameTimestamp: The BREAK start timestamp, in microseconds.
  7. packetTime: The duration of the last packet, in microseconds, measured from BREAK start to the end of the last slot.
  8. breakTime: The packet's BREAK time, set if RX line monitoring is enabled.
  9. mabTime: The packet's MAB time, set if RX line monitoring is enabled.

If the RX line is not being monitored, then the BREAK and MAB times will be set to zero.

There is also an optional parameter in readPacket, a PacketStats*, that enables retrieval of this data atomically with the packet data.

These metrics are reset to zero when the receiver is started or restarted.

Error statistics

Error statistics are tracked and the latest can be retrieved from an ErrorStats object returned by errorStats(). Included are these counts:

  1. packetTimeoutCount: Packet timeouts.
  2. framingErrorCount: Framing error count, including BREAKs that were too short.
  3. shortPacketCount: Packets that were too short.
  4. longPacketCount: Packets that were too long.

Synchronous operation by using custom responders

There is the ability to notify specific instances of Responder when packets having specific start codes arrive. To implement the simplest form, simply extend Responder, override the receivePacket function, and attach an instance to one or more start codes using Receiver::setResponder. receivePacket will be called for each packet received that has one of the desired start codes.

As well, by default, handlers will "eat" packets so that they aren't available to callers to the Receiver API. To change this behaviour, override Responder::eatPacket().

For example, let's say you want to change the local display when a text packet arrvies. The following partial code example shows how to do this.

class TextHandler : public teensydmx::Responder {
 public:
  static constexpr uint8_t kStartCode = 0x17;

  void receivePacket(const uint8_t *buf, int len) override {
    // The packet must contain at least 3 bytes (plus the start code)
    if (len < 4) {
      return;
    }
    uint8_t page = buf[1];
    uint8_t charsPerLine = buf[2];

    // Some checks should be made here for the data not ending in a
    // NUL character

    // Assume the existence of this function
    setText(page, charsPerLine,
            reinterpret_cast<const char *>(&buf[3]), len - 3);
  }
}

TextHandler textHandler;

void setup() {
  // ...
  dmxRx.setResponder(TextHandler::kStartCode, &textHandler)
  // ...

  dmxRx.begin();
}

Responders can be added at any time.

Complete synchronous operation examples using SIP and text packets can be found in SIPHandler and TextPacketHandler.

Responding

Protocols such as RDM need the ability, not only to process specific packets, but to respond to them as well. Timing is important, so a Responder implementation can also be notified of each byte as it arrives. The function of interest is processByte.

As bytes are received, the implementation tracks some internal state. When it is decided that a response is necessary, it returns a positive value indicating how many bytes it placed into the output buffer, for transmitting back to the transmitter. The Responder needs to implement outputBufferSize() in order for any response to be sent. processByte will be passed a buffer at least as large as the value returned from outputBufferSize().

Some other functions that specify some timings should also be implemented. Please consult the Responder.h documentation for more details.

Because all processing happens within an interrupt context, it should execute as quickly as possible. Any long-running operations should be executed in the main loop (or some other execution context). If the protocol allows for it, the Responder can reply with a "not yet" response, and then return any queued processing results when ready.

A more complete example is beyond the scope of this README.

DMX transmit

Code example

First, create an object using one of the hardware serial ports, different from any serial ports being used for receive:

namespace teensydmx = ::qindesign::teensydmx;

teensydmx::Sender dmxTx{Serial2};

Before using the instance, optionally set up the packet size and refresh rate, and then start the serial port internals to begin transmission:

// Optional: dmxTx.setPacketSize(100);
// Optional: dmxTx.setRefreshRate(40);
dmxTx.begin();

Set one or more channel values using one of the set functions:

// Set channel 6 to 219
dmxTx.set(6, 219);

The other set function can set multiple channels at once. This is left as an exercise to the reader.

Setting 16-bit values

Setting 16-bit values is easy with two available 16-bit functions:

  1. set16Bit for single words, and
  2. set16Bit for an array of words.

These work the same as the 8-bit set functions, but use the uint16_t type instead.

Packet size

The packet size can be adjusted and retrieved via setPacketSize and packetSize(). Smaller packets will naturally result in a higher refresh rate.

This can be changed at any time.

The minimum packet time is 1204us, and so as long as the actual time doesn't fall short of this, there is no minimum packet size. The equation is:

Packet Size >= max{(1204us - BREAK - MAB)/44us, 0}

Note that because the library operates asynchronously, if the packet size needs to be changed at the same time as the data, both must be set atomically. There are several ways:

  1. Disable the interrupts:

    __disable_irq();
    dmxTx.setPacketSize(...);
    dmxTx.set(...);
    __enable_irq();

    The disadvantage of this approach is that it disables all interrupts, possibly affecting the behaviour of other libraries. Instead of global interrupt disable, only the IRQ of the specific serial port in use should be disabled, but this is (currently) beyond the scope of this document.

  2. Pause and resume:

    dmxTx.pause();
    while (dmxTx.isTransmitting()) {
       yield();
    }
    dmxTx.setPacketSize(...);
    dmxTx.set(...);
    dmxTx.resume();

    The disadvantage of this approach is that it uses more code and effectively pauses execution.

  3. Use the setPacketSizeAndData() function:

    dmxTx.setPacketSizeAndData(...);

    This is probably the easiest approach.

Transmission rate

The transmission rate can be changed from a maximum of about 44Hz down to as low as you wish. See the setRefreshRate and refreshRate() in Sender.

Note that the rate won't be higher than the limits dictated by the protocol, about 44Hz, no matter how high it's set. The default is, in fact, INFINITY.

This can be changed at any time.

If the MBB time is also specified and it would conflict with the desired rate, then the larger of the two possible MBB times will be used to achieve either the specified rate or the specified MBB. See MBB time for more information.

Synchronous operation by pausing and resuming

Sender is an asynchronous packet transmitter; packets are always being sent. To ensure that certain packets are adjacent to others, such as for System Information Packets (SIP), the API provides a way to send packets synchronously.

Firstly, the pause() function pauses packet transmission, the resume() function resumes transmission, and resumeFor(int) resumes transmission for a specific number of packets, after which transmission is paused again.

There are two ways to achieve synchronous operation. The first is with isTransmitting(). It indicates whether the transmitter is sending anything while paused---it always returns true when not paused---and can be used to determine when it's safe to start filling in packet data after a resumeFor or pause() call.

The second way is to provide a function to onDoneTransmitting. The function will be called when the same conditions checked by isTransmitting() occur. It will be called from inside an ISR, so take this into account.

It is important to note that when utilizing the pause feature, changing data via the set functions should only be done while not transmitting. Pausing doesn't immediately stop transmission; the pause happens after the current packet is completely sent. Changing the data may affect this packet.

Let's say you want to send a SIP packet immediately after a regular packet. The following code shows how to accomplish this using the polling approach:

// Before the code starts looping, pause the transmitter
dmxTx.pause();

// Loop starts here
fillRegularData();
dmxTx.resumeFor(1);  // Send one regular packet
while (dmxTx.isTransmitting()) {  // Wait for this packet to be sent
  yield();
}
fillSIPData();
dmxTx.resumeFor(1);  // Send the SIP data
while (dmxTx.isTransmitting()) {  // Wait for this packet to be sent
  yield();
}

Using the asynchronous notification approach requires keeping track of some state, and is slightly more complex than the polling approach.

Other functions of interest are isPaused() and resumedRemaining(). isPaused() indicates whether the transmitter is paused (but still potentially transmitting). resumedRemaining() returns the number of packets that will be sent before the transmitter is paused again.

Complete synchronous operation examples using SIP can be found in SIPSenderAsync and SIPSenderSync. The first uses the asynchronous notification approach and the second uses the polling approach.

Choosing BREAK and MAB times

The BREAK and MAB times can be specified in two ways:

  1. By specifying specific times using the setBreakTime and setMABTime functions, or
  2. By specifying serial port parameters and relying on multiples of bit times, using the setBreakSerialParams function.

The default times are set, in both cases, to approximately 180us for the BREAK and approximately 20us for the MAB.

The mode is switched between these two options using the setBreakUseTimerNotSerial function. The default is to use serial parameters.

Specific BREAK/MAB times

This is the first way to generate these times.

The BREAK will be transmitted with a duration reasonably close to the specified value, but the actual MAB time may be larger than requested. This has to do with how the UARTs on the chip work.

This feature uses one of the PIT timers via PeriodicTimer (or, optionally, the IntervalTimer API), but if none are available, then the transmitter will fall back on using the baud rate generator with the specified serial port parameters.

A note on BREAK timing

The BREAK timing is pretty accurate, but slightly shorter and longer times have been observed.

(If using the IntervalTimer API, there's some inaccuracy. It doesn't provide a way to execute an action (in this case, starting a BREAK) just before the timer starts. Instead, the timer will have already started and some time elapsed before the BREAK can start.

Efforts have been made to make the BREAK time be at least the amount requested, but it likely won't be exactly the requested duration.)

A note on MAB timing

The MAB time may be longer than requested. The reason is that it's not possible to immediately start sending a character using the UART on the Teensy chips. It seems that the best resolution one can get is "somewhere within one or two bit times".

To illustrate: BREAK ->(immediate) MAB ->(not immediate) First packet character

For example, if a timer aims for a 23us MAB, and then a character is sent to the UART after the timer expires, then that character might start after 25us. If the timer is adjusted so that it expires 2us earlier, then the character might still appear too late, still after 25us, for example. A further adjustment of 2us, for a timer duration of 19us, might result in the character appearing after 21us, earlier than requested.

When a character starts, it appears that the UART performs its functions asynchronously, and so it is not possible to achieve exact timing. The code adjusts the MAB time under the covers to achieve times that are as close to the requested time as possible without going under, but it is often not possible to be more precise than "within one or two bit times", depending on the processor.

BREAK/MAB times using serial parameters

This is the second way to generate these times.

The times are restricted to having a BREAK:MAB ratio of 9:2, 10:2, 9:1, 10:1, 9:3, 8:2, or 11:1. These correspond to the UART formats, 8N2, 8E2, 8N1, 8E1, 8O2, 7O1, and 9E1. For additional information on this subject, see the BREAK Timing in DMX512-A note.

The BREAK time will be fairly accurate, but the MAB time will be a little longer than expected, by up to at least several bit times.

This is a less flexible way to specify the BREAK and MAB times. The baud rate is changed just for these and then changed back to 250kbaud (8N2) for the packet data. The act of changing the baud rate can introduce a delay, somewhere up to one full character.

This mode is also used as a fallback if the system doesn't have the timers available.

Inter-slot MARK time

The inter-slot MARK time can be set with the setInterSlotTime function and retrieved using the interSlotTime() function. Note that the MARK time should be accurate to within one or two bit times due to internal UART details. See A note on MAB timing for more information.

MBB time

The MARK before BREAK (MBB) time can be set with the setMBBTime function and retrieved using the mbbTime() function. Note that the time should be accurate to within one or two bit times due to internal UART details. See A note on MAB timing for more information.

Note also that there will always be some minimum transmitted MBB due to how the code and UART interact.

If the refresh rate is set as well then the actual MBB time will be whichever makes the packet larger. For example, if the MBB is set to 100us, but adding this to the packet would result in a rate that's slower than specified, then 100us is used. On the other hand, if adding the same MBB would make the specified rate faster, then enough additional time will be added so that the rate is correct.

Error handling in the API

Several Sender functions that return a bool indicate whether an operation was successful. Prior versions of the library did nothing with, or silently ignored, bad arguments.

The status-returning functions are as follows:

  1. setPacketSize,
  2. Both set functions,
  3. Both set16Bit functions,
  4. setRefreshRate, and
  5. Both resumeFor functions.

Technical notes

Simultaneous transmit and receive

The same serial port can't be used for simultaneous transmit and receive. This is because the library uses the serial port hardware for data instead of direct pin control. The break portion of a DMX frame needs to be transmitted at a different baud rate than the slots (channels), and since reception and transmission aren't necessarily synchronized, two different serial ports must be used.

Use qindesign::teensydmx::Receiver to receive and qindesign::teensydmx::Sender to transmit.

Transmission rate

From a UART perspective, there are two parts to a DMX frame:

  1. BREAK, 50000 baud, 8N1.
  2. Up to 513 slots, 250000 baud, 8N2.

The total frame time is approximately:

10 bits * 20 us + 513 slots * 11 bits * 4us = 22772us, or a rate of about 43.91Hz.

The total frame time may be a little longer due to switching baud rates internally and the existence of some interrupt and code execution latency.

Transmit/receive enable pins

Some setups may require that an external part be enabled when transmitting or receiving. For example, an RS485 transceiver may require enabling or disabling specific buffers. That may be accomplished by using one of the GPIO pins. Please be sure the logic levels are compatible.

Thread safety

This code is not thread-safe and should be handled appropriately if utilized in a concurrent context.

Dynamic memory allocation failures

The Receiver::setResponder function dynamically allocates memory. On small systems, this may fail. The caller can check for this condition by examining errno for ENOMEM. If this occurs, then the function will return nullptr, but otherwise fails silently. Additionally, all responders are wiped out, including any previously-set responders.

Hardware connection

DMX uses RS-485 differential signalling. This means that a transceiver is needed between the Teensy and the DMX lines. See DMX512 for DMX connector pin guidance.

  1. Basically, DMX uses A, B, and ground, and the Teensy serial ports use RX, TX, and ground. After choosing a serial port on the Teensy, connect TX to the Driver Input and RX to the Receiver Output of the transceiver.
  2. Since RS-485 is half-duplex, the transceiver provides a way, via either one or two pins, to select between receiving and transmitting modes. For the two-pin case, these likely can be merged because they're logically opposite, for example, HIGH to enable the driver and LOW to enable the receiver. In both cases, choose one of the Teensy's digital output pins to act as the transmit-or-receive selector.
  3. Last, connect the ground of the Teensy to the ground of the transceiver.

It is beyond the scope of this document to describe how to accommodate transmission line effects of long lines.

Receiver and driving the TX pin

By default, when a Receiver is used, the TX pin for its serial port is enabled. This means that the line is driven. This default state was chosen because custom responders may wish to transmit information. Additionally, the caller does not need to remember to enable the transmitter when adding a custom responder.

If it is known that there are no responders or that no responders will send data, then the TX pin can be disabled so that the UART hardware isn't driving the line. The Receiver::setTXEnabled function can be called either during operation or outside a begin and end pair.

Be aware that if the receiver is currently in operation, enabling the transmitter will cause an 11-bit idle character to be queued for output and the line to remain high after that.

Potential PIT timer conflicts

By default, this library internally uses PIT timers via Teensy's default IntervalTimer API. For more accurate BREAK timing, a custom API, PeriodicTimer, can be used instead. Globally define the TEENSYDMX_USE_PERIODICTIMER macro when building and the library will use this custom API. However, be aware that conflicts may occur if other libraries in your project use IntervalTimer.

Code style

Code style for this project mostly follows the Google C++ Style Guide.

Other conventions are adopted from Bjarne Stroustrup's and Herb Sutter's C++ Core Guidelines.

References

  1. BREAK Timing in DMX512-A

Inspirations for this library:

  1. Chris Staite's TeensyDmx library, which is further based on Matthias Hertel's DMXSerial2, Ward's DmxReceive, and Paul Stoffregen's DmxSimple.
  2. Claude Heintz's LXTeensy3DMX_Library.

Acknowledgements

  1. It's one thing to have chip documentation, but it's an entirely new beast when there exists code that already implements all the hard parts. Thank you to Paul Stoffregen for all the Teensy source code. This helped immensely when developing this library. See: https://github.com/PaulStoffregen/cores

Copyright (c) 2017-2021 Shawn Silverman

teensydmx's People

Contributors

ssilverman avatar

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

Watchers

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

teensydmx's Issues

Teensy 3.6 Serial6 doesn’t work in conjunction with other ports

From an email:

I am trying to use Serial6 for a teensyDMX Sender running on a Teensy 3.6 and can’t seem to get it to work.
The other 5 serial outputs work well and I can use those 5 and create a Sender with Serial6.
It’s when I call begin on the Serial6 Sender that things break down. I’m not sure what is happening but none of the other serial outputs work after that call.
Searching on the Teensy forum I found a message from 2019 where you mention an LPUART issue, could I be running into that?
If you haven’t had a chance to test Serial6 on a Teensy 3.6 in the mean time, and are still interested in it, I can run tests and check the outputs on a scope.

More details:

  1. Serial6 works by itself.
  2. Just Serial6 and Serial1 doesn’t work.

USBProWidget - usb type

If I am to understand correctly this example makes the Teensy function like one of the well-known brands DMX Pro interface for use with apps like QLC? How stable is is considered?

I tried to flash, but it's not detected correctly in QLC+ (my other real devices are). Permissions error or am I selecting the wrong USB Type opinion in the IDE? The file doesn't state the correct mode to use

Not receiveng DMX on teensy 4 with RX monitor on a specific dmx transmitter

Hi There!

I've been playing with the code for a long time now and I just encountered a problem that I can not figure a solution yet.
The problem is that I'm unable to receive valid DMX data from one of my DMX transmitters with the teensyDMX library.
I've tried with a hardware node and there I can receive correctly the DMX information coming from the same transmiter.
Clarify that the same code is sucsessfully receiving data coming from a second dmx transmiter from a different brand. So the problem seems to be the format of the DMX data.

I've enabled DMX monitor on pin 6 of the teensy 4,0 but without any luck. Also enabled setKeepShortPackets.

here is my code:

`/*

  • A basic toy receive example, demonstrating single- and
  • multi-byte reads.
  • This example is part of the TeensyDMX library.
  • (c) 2019-2020 Shawn Silverman
    */

#include

#include <TeensyDMX.h>

namespace teensydmx = ::qindesign::teensydmx;
elapsedMillis ledTimer;

// Create the DMX receiver on Serial2.
teensydmx::Receiver dmxRx{Serial2};

///////////////////////////////////////////////////////////////////////
// The last value on the channel, for knowing when to print a change
// (Example 1).
uint8_t lastValue = 0;

// Buffer in which to store packet data (Example 2).
uint8_t packetBuf[3]{0};

// The last values received on channels 10-12, initialized to zero.
uint8_t rgb[3]{0};
///////////////////////////////////////////////////////////////////////

void setup() {

// Turn on the LED, for indicating activity
pinMode(LED_BUILTIN, OUTPUT);
digitalWriteFast(LED_BUILTIN, HIGH);

// initate XBEE serial com
Serial1.begin (9600);

// Serial initialization, for printing things
Serial.begin(115200);
while (!Serial && millis() < 4000) {
// Wait for initialization to complete or a time limit
}
Serial.println("Starting BasicReceive + send XBEE");

// Start the receiver
dmxRx.begin();
int DMXwachPIN = 6;
pinMode (DMXwachPIN, INPUT);
dmxRx.setRXWatchPin (DMXwachPIN);
dmxRx.setKeepShortPackets(true);

// Print the first values
lastValue = dmxRx.get(1);
// Serial.printf("Channel 1: %d\n", lastValue);

delay (1000);
}

void loop() {
// The following two examples print values when they change

// Example 1. Get the current value of channel 1.
// This will return zero for no data (and also for data that's zero)
uint8_t v = dmxRx.get(1);
if (v != lastValue) {
lastValue = v;
Serial1.write (v); // Send to xbee
// Serial.printf("Channel 1: %d\n", v);
setLedAnalog(v);
}

if (ledTimer>300) {
setLedAnalog(0);
}

// print any received info from xbee
if (Serial1.available() > 0) {
int data = Serial1.read();
Serial.println (data,HEX);
}
}

void setLed(bool stateLed) {
digitalWriteFast(LED_BUILTIN, stateLed);
ledTimer = 0;
}

void setLedAnalog(uint8_t stateLed) {
analogWrite(LED_BUILTIN, stateLed);
ledTimer = 0;
}`

And the scope pictures of the different DMX signals starts (working and not working):

Working signal
WORKING SIGNAL

Not working signal
NOT WORKING SIGNAL

Any idea of what could be going wrong? Is there anything else I could try that comes to mind?
Thanks for this wonderfull library!

using teensy 4.1 for 8 DMX universes

Does any one have any experience with using the teensy 4.0 or 4.1 as a multi universe DMX interface?
I wonder if it could be used as a replacement for the ENTTEC Storm 8

I know the Storm 8 is an Art-Net device. So either try to use https://github.com/natcl/Artnet or expand the USBProWidget example to handle multiple universes.

I would also need to explore how to expand the ofxDMX openframeworks c++ addon i am currently using so it sends out more than 2 universes.

I am happy to hear if others have done something like this before.

TeensyDMX.h:20:19: fatal error: cstdint: No such file or directory

Hello,

this library sounds awesome, i've been thinkin about doing some DMX projects on my Arduinos for Months.
Unfortunately, trying to compile a Sketch using the lib yields the following error:

libraries\TeensyDMX\src/TeensyDMX.h:20:19: fatal error: cstdint: No such file or directory

Full Sketch:

`
#include <TeensyDMX.h>

namespace dmx = ::quindesign::teensydmx;

// send / Drivers
#define DE 2
// receive
#define RE 3

dmx:Sender sender{Serial};

void setup() {
// put your setup code here, to run once:

}
void loop() {
// put your main code here, to run repeatedly:
}
`

Library Version: 3.0.0-beta installed from Arduino Library Manager
Arduino Version: 1.8.7

Receiver is unable to parse RB-DMX1

I'm seeing framing errors and other issues when trying to receive data from a Pioneer RB-DMX1.

My setup is as follows:

  • Teensy 4.1
  • TeensyDMX was pulled from git, from master branch not more than a few days ago
  • Sketch is DMX receiver-only, via Serial1. Based on the example sketch.
  • I'm using pin 3 as an RX watch pin, via dmxRx.setRXWatchPin(RX_WATCH_PIN)
  • I'm using the serial console to debug packet stats and error information to the serial monitor.

The RB-DMX1 works fine when I hook it up directly to my lights. I've got roughly 19 DMX devices and about 157 used channels.

The Teensy 4.1 and your TeensyDMX library work fine if I receive DMX data from an Enttec Open DMX, with the same amount of fixtures.

When the RB-DMX1 is sending data to TeensyDMX, I will get framing errors and long packet counts. Depending on what the DMX box is outputting, it will sometimes not give any errors at all, but in other situations, it will give me nothing but errors, with no data at all output. This box is proprietary in that it only works with Rekordbox DJ software. what's weird is that it seems to wig out the most when it's sending certain colors, like red. Other colors/modes, it won't error out at all. Which is really weird.

I started adding debug code in to figure out where the framing errors are coming from. There are about 11 places in the code where receiveBadBreak() will increment framingErrorCount.
I see framing error counts increase in the following source locations:

The DMX output looks decent on an oscilloscope but my scope kinda sucks. I think I might modify the library to pulse a spare IO pin when the MAB transitions. that would be helpful. But anyway, here are the measurements I can come up with from the scope:

  • Space-for-break time: 180.0 uS
  • MAB time: ~20 uS
  • Data bit length: 4 uS
  • Break-to-break time: ~28.8 ms

The times look really consistent, there's not really any jitter to speak of.

I've spent a few days on this and I'm not sure what to do from here. I'm pretty certain that the DMX device is within spec, or close to it, especially since it works with my lights. I think this might a legitimate bug with the library. Could you help me with this?

Serial2-6 for recieve on Teensy 3.6

Seems like Serial3 and Serial4 (so far as I tested) are not working for recieve on my Teensy 3.6. If I connected a DMX Singal to Serial1 (Pin0) or Serial2 (pin9) everything works fine. Connecting it to Serial3 or Serial4, I cant recieve any values. I'm using the Flasher.ino provided in your examples.

Would be nice if you can fix that :)
Or is this just a problem on my side?

Freeze when using Audio Shield and DMX transmit on Serial3

Original discussion is here:
https://forum.pjrc.com/threads/71964-Using-Serial3-when-using-audio-library

In summary, it appeared that the Audio Shield was causing some freeze when transmitting DMX on Serial 3. Upon further digging, it was discovered that it was some interaction with TeensyDMX, plus the Audio Shield capacitor on pin 15 (RX3).

Test code:

#include <TeensyDMX.h>
// #include <Audio.h>

namespace teensydmx = ::qindesign::teensydmx;
teensydmx::Sender dmxTx{Serial3};

// AudioOutputI2S           audioOutput;

void setup() {
  Serial.begin(115200);
  delay(1500);
  if (CrashReport) {
    Serial.println(CrashReport);
    CrashReport.clear();
  } else {
    Serial.println("No CrashReport");
  }
  Serial.println("setup begin");

  dmxTx.begin();

  for (int i=0; i<25; i++) {
    Serial.println(i);
    delayMicroseconds(250);
  }
  Serial.println("setup end");
}

void loop() {
  static elapsedMillis timer;
  static uint32_t counter = 0;
  if (timer >= 1000) {
    Serial.println(counter++);
    timer = 0;
  }
}

The cause is a capacitor connecting the RX line to voltage, eventually triggering RX and causing continuous TX. i.e. When RX reaches a threshold. The best scope trace that shows this is in post # 15:
https://forum.pjrc.com/threads/71964-Using-Serial3-when-using-audio-library?p=319218&viewfull=1#post319218
file

The blue trace shows that the TX interrupt seems to be continuously triggering after RX reaches some threshold. Code to generate this (on Serial1):

void lpuart6_tx_isr() {
digitalWriteFast(32, HIGH); // Blue trace on scope
  Sender *s = txInstances[0];
  if (s != nullptr) {
    s->sendHandler_->irqHandler();
  }
digitalWriteFast(32, LOW);
}

(Credit to @PaulStoffregen for the trace.)

Teensy Locks Up after several Connects / Disconnects of DMX from certain sources.

It seems to be possible to to cause the Teensy (Teensy 3.2 (96Mhz (Overclocked)) to lock up when receiving DMX from certain lighting desks and physically connecting / disconnecting the dmx cable.

The timings of the Offending DMX source are as follows:
Refresh: 20Hz
Break Length: 238 to 280uS (**)
MAB: 23uS
Channels Rx: 512

When the lock up occurs, only a power cycle fixes it .

USBWidgetPro Example Correction

Hi,

First of all, thank you for this great library!

I've been using this for a couple of projects of mine, and it works very well. One of the projects is to upgrade the Velleman VM116/K8062 USB Controller DMX Interface by removing the original PIC and replacing it with a TeensyLC running TeensyDMX in send mode. I essentially used the USBWidgetPro example pretty much as is, and it works well with FreeStyler and the fixtures I tested it on.

However, when I tried to use it controlling another one of my projects, I was getting quite a lot of glitches coming through. To cut a long story short, it turned out that the problem was that the TeensyLC was pausing briefly to process data received from the USB port.

The issue is this block of code in handleMessage (line 508):

if (len != dmxTx.packetSize()) {
   __disable_irq();
   dmxTx.setPacketSize(len);
   dmxTx.set(0, msg.data, len);
   __enable_irq();
   } else {
     dmxTx.set(0, msg.data, len);
}

The data can be set whilst a transmission is taking place, causing a brief delay in the DMX data stream and glitching the receiver. I propose changing this block to:

if (len != dmxTx.packetSize()) {
  dmxTx.pause();
  while(dmxTx.isTransmitting()) { yield(); }
  dmxTx.setPacketSize(len);
  dmxTx.set(0, msg.data, len);
  dmxTx.resume();
} else {
  dmxTx.pause();
  while(dmxTx.isTransmitting()) { yield(); }
  dmxTx.set(0, msg.data, len);
  dmxTx.resume();
}

After this change, the glitches disappeared and it all works perfectly!

I hope it helps!

Teensy 4.1 - No Serial8

Using the BasicSend.ino example works on the 4.1 with Serial1-Serial7, but not on Serial8. It will not fail to compile or during runtime, but there is no output. I tried to dig through and did notice the switch for Serial8 references the wrong IC when checking defines (IMXRT1052 instead of IMXRT1062 on the 4.0 and 4.1, and I believe the 4.1 is the only model with Serial8 available)
I tried changing that but resulted in a slew of errors. If you do not have a 4.1 available to test, I can try any updates you make to confirm. I'm just not sure where to start to try to find the issue myself.

On another note, I'm also using NativeEthernet on this board and teensyDMX seems to kill the linkStatus functionality due to link_callback not being received, yet works fine without this lib initialized.. don't expect a definitive answer but if you have ANY idea what in here may conflict with that so I could dig a bit I would love to hear.. been fighting that one for a while..

Example pinout?

Novice here, is their any examples for using this library with a 3pin XLR DMX light?

Also is an RS485 needed or could a Teensy LC be used standalone with the light

Returning all Zeroes, Packet time negative (-44)

Hi

I'm using your library, but I'm having problems reading the data.

Using teensydmx::Receiver::readPacket i'm trying to read 2 channels, sporadically both are setted to 0 even if the function has return value 2 (channel readed) , when this happen Receiver::PacketStats::packetTime (obtained passing it as forth function parameter) has value -44

This behavior is the same with both and without teensydmx::Receiver::setRXWatchPin configured

I'm also running 2 analogWrite, both using FTM1 as timer

I'm running on a teensy3.2, TeensyDMX4.0.0, compiled on Platformio (Teensyduino 1.53)

Any ideas? Thanks

Use of connection();

Hi,

I try to detect whether my DMXreceiver receives a dmx signal. I wanted to use the connected() function. However, I don't seem to get it working. I think the docs are not written for an Intermediate level programmer so I'm a bit stuck.

I would like to keep looping a function when no DMX signal is detected.
The example code in the docs gives an error. I try to put something together myself but also had no luck so far.
What would be the correct implementation of connection detection for my use case?

Docs example:

dmxRx.onConnectChange([](Receiver *r) {
  digitalWriteFast(LED_BUILTIN, r.connected() ? HIGH : LOW);
});

My try:

  while (dmxRx.connected() == false)
  {
  Serial.println("No DMX signal");
  markColor(100, 255, 0, 0); //NUM_LEDS 
  }

Full code

#include <TeensyDMX.h>
namespace teensydmx = ::qindesign::teensydmx;
teensydmx::Receiver dmxRx{Serial1};
uint8_t packetBuf[5] {0};
uint8_t dsrgb[5] {0}; //Dimmer, RGB, strobe


// BasicTest example to demonstrate how to use FastLED with OctoWS2811
#include <OctoWS2811.h>
#define USE_OCTOWS2811
#include <FastLED.h>
#define NUM_LEDS  190 //3040

CRGB leds[NUM_LEDS];

void setup() {
  Serial.begin(115200);
  while (!Serial && millis() < 4000) {
    // Wait for initialization to complete or a time limit
  }

  while (dmxRx.connected() == false)
  {
  Serial.println("No DMX signal");
  markColor(100, 255, 0, 0); //all leds red
  }


  LEDS.addLeds<OCTOWS2811, RGB>(leds, NUM_LEDS / 8);
  Serial.print("Let there be light!");
  markColor(100, 255, 0, 0);
  delay(2000);
  Serial.println("Done");
  Serial.println("Start DMX read");
  dmxRx.begin();
}

void loop() {
  dmxRx.readPacket(dsrgb, 1, 5); // first 5 channels

  //Dimmer + color
  if (dsrgb[4] < 10) {
    markColor(dsrgb[0], dsrgb[1], dsrgb[2], dsrgb[3]);
  }

  //STROBE + color
  if (dsrgb[4] > 10) {
    Strobe(map(dsrgb[4], 10, 255, 10, 150)); //argument: strobe time in MS
  }

}



void Strobe(uint8_t strobeIntervalMs) {
  if ((millis() / strobeIntervalMs) % 2) {
    markColor(0, 0, 0, 0);
  } else {
    markColor(dsrgb[0], dsrgb[1], dsrgb[2], dsrgb[3]);
  }
}

void markColor(uint8_t dmxDimmer, uint8_t dmxRed, uint8_t dmxGreen, uint8_t dmxBlue) {
  uint8_t dimmerRed = map(dmxRed, 0, 255, 0, dmxDimmer);
  uint8_t dimmerGreen = map(dmxGreen, 0, 255, 0, dmxDimmer);
  uint8_t dimmerBlue = map(dmxBlue, 0, 255, 0, dmxDimmer);

  for (int i = 0; i < NUM_LEDS; i++) {
    leds[i].setRGB(dimmerRed, dimmerGreen, dimmerBlue);
  }

  FastLED.show();
}

Teensy4.1 multi DMX Sender.

How many serial ports can be used at the same time?
I want to use all 8 serial ports on Teensy4.1 to send out DMX signals.
By modifying the sample code and adding the number of serial ports one by one, there is no problem at around 3, but beyond that, the processor dies.

Test Code..

#include <TeensyDMX.h>

namespace teensydmx = ::qindesign::teensydmx;

constexpr uint8_t kTXPin = 17;

teensydmx::Sender dmxTx{Serial1};
teensydmx::Sender dmxTx2{Serial2};
teensydmx::Sender dmxTx3{Serial3};
teensydmx::Sender dmxTx4{Serial4};
teensydmx::Sender dmxTx5{Serial5};
teensydmx::Sender dmxTx6{Serial6};
teensydmx::Sender dmxTx7{Serial7};
teensydmx::Sender dmxTx8{Serial8};

// Data for 3 channels.
uint8_t data[3]{0x44, 0x88, 0xcc};

void setup() {
Serial.begin(128000);
while (!Serial && millis() < 4000) {
}
// Turn on the LED, for indicating activity
pinMode(LED_BUILTIN, OUTPUT);
digitalWriteFast(LED_BUILTIN, HIGH);

// Set the pin that enables the transmitter; may not be needed
pinMode(kTXPin, OUTPUT);
digitalWriteFast(kTXPin, HIGH);

dmxTx.set(1, 128);
'
dmxTx.set(10, data, 3);

dmxTx.begin();
dmxTx2.begin();
dmxTx3.begin();
// dmxTx4.begin();
// dmxTx5.begin();
// dmxTx6.begin();
// dmxTx7.begin();
// dmxTx8.begin();
}

void loop() {
Serial.print( dmxTx.packetSize());
Serial.print(" ");
Serial.print( dmxTx2.packetSize());
Serial.print(" ");
Serial.print( dmxTx3.packetSize());
Serial.print(" ");
Serial.print( dmxTx4.packetSize());
Serial.print(" ");
Serial.print( dmxTx5.packetSize());
Serial.print(" ");
Serial.print( dmxTx6.packetSize());
Serial.print(" ");
Serial.print( dmxTx7.packetSize());
Serial.print(" ");
Serial.println( dmxTx8.packetSize());
//Serial.println( dmxTx.refreshRate());
dmxTx.set(10,data,3);
dmxTx2.set(10,data,3);
dmxTx.setPacketSize(data[0]);
dmxTx2.setPacketSize(data[0]+10);
dmxTx3.setPacketSize(data[0]+20);
// dmxTx4.setPacketSize(data[0]+30);
// dmxTx5.setPacketSize(data[0]+40);
// dmxTx6.setPacketSize(data[0]+50);
// dmxTx7.setPacketSize(data[0]+60);
// dmxTx8.setPacketSize(data[0]+70);
data[0]++;

delay(100);
}

Finish Teensy 4 support

This is written but I still have to test on some hardware. I have some Teensy 4s but haven’t tested yet.

Any plans for a DMX USB Pro Widget implementation?

Hi Shawn,

Thanks for your great work on this library, it's awesome to have a functional interface for working with DMX on the Teensies.

I saw you briefly mentioned that the library should be in a state where the widget API could be implemented on top and was wondering if you have a reference implementation or plans to integrate such functionality into TeensyDMX. Being able to do so would make it perfect for use in headless setups with DMX recorders etc. and without having to user ArtNet and additional expensive Ethernet breakout.

I understand the DMX over USB widget API is essentially a serial signaling spec but I'm not entirely sure where to begin in implementing it against software like MadMapper / TouchDesigner or ay other PC client.

Any help / pointers / reference code would be appreciated!

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.