Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save psmay/8794c019c581265feb25 to your computer and use it in GitHub Desktop.
Save psmay/8794c019c581265feb25 to your computer and use it in GitHub Desktop.
Early prototype Arduino sketch for bidi SextetStream I/O
Copyright (c) 2015 Peter S. May
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
#ifndef ARRAYS_H
#define ARRAYS_H
// First argument of all these macros must be statically allocated.
#define ARRAY_LENGTH(a) ( sizeof(a) / sizeof(a[0]) )
#define ZERO_ARRAY(a) (memset(a, 0, sizeof(a)))
#define ARRAYS_EQUAL(a, b) (memcmp(a, b, sizeof(a)) == 0)
#define COPY_ARRAY(dst, src) (memcpy(dst, src, sizeof(dst)))
#endif
#ifndef BUTTONINFO_H
#define BUTTONINFO_H
struct ButtonAddress {
uint8_t byteIndex;
uint8_t bitMask;
};
struct ButtonInfo {
uint8_t pinNumber;
ButtonAddress address;
};
inline ButtonAddress getButtonAddress(uint16_t zeroBasedIndex) {
uint8_t byteIndex, shift;
byteIndex = (byte)(zeroBasedIndex / 6);
shift = zeroBasedIndex % 6;
ButtonAddress address = { byteIndex, 1 << shift };
return address;
}
#endif
#ifndef LIGHTADDRESSES_H
#define LIGHTADDRESSES_H
// This is not a complete list of the light addresses available. See
// `LightsDriver_SextetStream.md` for details.
// TODO: Replace with complete list
const LightAddress
LIGHT_MARQUEE_UL = { 0, 0x01 },
LIGHT_MARQUEE_UR = { 0, 0x02 },
LIGHT_MARQUEE_LL = { 0, 0x04 },
LIGHT_MARQUEE_LR = { 0, 0x08 },
LIGHT_BASS_L = { 0, 0x10 },
LIGHT_BASS_R = { 0, 0x20 },
LIGHT_P1_B1 = { 3, 0x01 },
LIGHT_P1_B2 = { 3, 0x02 },
LIGHT_P1_B3 = { 3, 0x04 },
LIGHT_P1_B4 = { 3, 0x08 },
LIGHT_P1_B5 = { 3, 0x10 },
LIGHT_P1_B6 = { 3, 0x20 };
// In dance mode, these mappings exist.
#define LIGHT_P1_PAD_L LIGHT_P1_B1
#define LIGHT_P1_PAD_R LIGHT_P1_B2
#define LIGHT_P1_PAD_U LIGHT_P1_B3
#define LIGHT_P1_PAD_D LIGHT_P1_B4
#endif
#ifndef LIGHTINFO_H
#define LIGHTINFO_H
struct LightAddress {
uint8_t byteIndex;
uint8_t bitMask;
};
struct LightInfo {
uint8_t pinNumber;
LightAddress address;
};
#endif
#if (ARDUINO >= 100)
#include <Arduino.h>
#else
#include <WProgram.h>
#endif
// vim:ft=arduino
// To customize, skip to the bit that says START CUSTOM SETTINGS.
#include "platform.h"
#include "arrays.h"
#include "LightInfo.h"
#include "LightAddresses.h"
#include "ButtonInfo.h"
//
// START CUSTOM SETTINGS
//
const LightInfo LIGHT_INFO[] = {
// { Pin, address }
//{ 2, LIGHT_P1_PAD_L },
//{ 3, LIGHT_P1_PAD_D },
//{ 4, LIGHT_P1_PAD_U },
//{ 5, LIGHT_P1_PAD_R }
{ 2, LIGHT_MARQUEE_LL },
{ 3, LIGHT_MARQUEE_UL },
{ 4, LIGHT_MARQUEE_UR },
{ 5, LIGHT_MARQUEE_LR }
};
const byte LIGHT_COUNT = ARRAY_LENGTH(LIGHT_INFO);
const ButtonInfo BUTTON_INFO[] = {
// { Pin, address }
{ A3, getButtonAddress(0) },
{ A2, getButtonAddress(1) },
{ A1, getButtonAddress(2) },
{ A0, getButtonAddress(3) }
};
const byte BUTTON_COUNT = ARRAY_LENGTH(BUTTON_INFO);
// A change in button states will not be output until it has been stable for
// at least this many ms.
const unsigned long OUTPUT_DEBOUNCE_TIMEOUT = 20;
// If this many ms have passed since the last output by the time the run
// loop has reached the update, output will be forced even if the previous
// output was the same. Note that this is not preemptive (interrupts are not
// used and other activity in the run loop may need to finish before the
// time is checked).
const unsigned long OUTPUT_UPDATE_TIMEOUT = 1000;
//
// END CUSTOM SETTINGS
//
//
// Sextet fix-up
//
// In this context, when we say "Sextet" we're more or less talking about an
// 8-bit byte whose low 6 bits have been left alone but whose high 2 bits
// have been twiddled so that the result is in 0x30..0x6F, which are all
// 7-bit-clean, printing, non-whitespace, non-control ASCII characters.
//
// 0x00-0x0F -> 0x40-0x4F
// 0x10-0x1F -> 0x50-0x5F
// 0x20-0x2F -> 0x60-0x6F
// 0x30-0x3F -> 0x30-0x3F
//
// (The ranges 0x20-0x2F and 0x70-0x7F were consciously avoided because they
// contain unsafe characters.)
//
// This mapping allows the formatted data to pass through text-only channels
// unharmed. Additionally, characters like newlines, commas, tabs, and null
// bytes are outside this range and available for use as delimiters.
//
// Because the low 6 bits remain unmodified, the ASCII code for the Sextet
// can be ANDed to 0x3F to retrieve the unmapped value. In applications
// where only the individual bit values are used, such as this one, this
// part is not necessary.
// Maps the low 6 bits of the given byte to a Sextet. (This operation is
// idempotent.)
inline byte fixUpSextet(byte data) {
return ((data + (byte)(0x10)) & (byte)(0x3F)) + (byte)0x30;
}
// Outputs the buffer as a line of Sextets suffixed with a newline.
inline void outputBufferAsSextets(const byte * buf, byte count) {
byte i;
// Write out as fixed-up sextets
for (i = 0; i < count; ++i) {
Serial.write(fixUpSextet(buf[i]));
}
// Add newline
Serial.write(0x0A);
}
//
// Light data buffer
//
const byte LIGHT_DATA_BUFFER_SIZE = 13;
byte lightData[LIGHT_DATA_BUFFER_SIZE];
byte lightDataIndex = 0;
// Clears and resets the light data buffer.
inline void resetLightData() {
lightDataIndex = 0;
ZERO_ARRAY(lightData);
}
// Append a byte to the light data buffer.
// If the buffer is already full, returns false.
// Otherwise, appends to the buffer and returns true.
inline bool putLightData(byte data) {
if (lightDataIndex >= LIGHT_DATA_BUFFER_SIZE) {
return false;
}
else {
lightData[lightDataIndex] = data;
lightDataIndex++;
return true;
}
}
// Returns true iff the light data buffer is empty.
inline bool lightDataBufferIsEmpty() {
return (lightDataIndex == 0);
}
// Use the data in the light data buffer to update the light states.
void refreshLights() {
byte i;
for (i = 0; i < LIGHT_COUNT; ++i) {
byte index = LIGHT_INFO[i].address.byteIndex;
byte bits = LIGHT_INFO[i].address.bitMask;
bool state = (lightData[index] & bits) != 0;
digitalWrite(LIGHT_INFO[i].pinNumber, state);
}
}
// Receive a byte from serial.
// If it ends a line with a nonzero width, update the light output using the
// current buffer.
// Otherwise, if it looks like valid data, append it to the buffer
// (discarding on overflow).
// Otherwise, ignore the value.
inline void receiveLightData(byte data) {
if ((data == 0x0A) || (data == 0x0D)) {
// Line is finished
if (!lightDataBufferIsEmpty()) {
// Update the lights
refreshLights();
resetLightData();
}
// Otherwise, skip as a spurious extra newline.
// A "clear all" message contains of one or more '@' (0x40 -> 0x00).
}
else if ((data >= 0x30) && (data <= 0x6F)) {
// Valid data byte
putLightData(data);
}
// Otherwise, result is undefined, so here we'll just ignore.
}
//
// Button data buffer
//
const byte BUTTON_DATA_BUFFER_SIZE = 4;
byte lastButtonData[BUTTON_DATA_BUFFER_SIZE];
byte pendingButtonData[BUTTON_DATA_BUFFER_SIZE];
bool debounceIsPending = false;
unsigned long buttonDataRepeatExpiry;
unsigned long buttonDataDebounceExpiry;
// Resets all button data to 0 bits.
inline void resetButtonData() {
ZERO_ARRAY(lastButtonData);
ZERO_ARRAY(pendingButtonData);
buttonDataRepeatExpiry = buttonDataDebounceExpiry = millis();
}
// Output the data in the button data buffer as fixed-up sextets with a
// newline at the end.
inline void outputButtonDataAsSextets() {
outputBufferAsSextets(lastButtonData, BUTTON_DATA_BUFFER_SIZE);
}
// Reads the button inputs and ORs corresponding bits onto the given button
// data buffer.
inline void collectButtonDataFromInputs(byte newButtonData[]) {
byte i;
for (i = 0; i < BUTTON_COUNT; ++i) {
bool state = !digitalRead(BUTTON_INFO[i].pinNumber);
if (state) {
byte index = BUTTON_INFO[i].address.byteIndex;
byte bits = BUTTON_INFO[i].address.bitMask;
newButtonData[index] |= bits;
}
}
}
inline bool timeExpired(unsigned long expiry, unsigned long now) {
return (((signed long)now) - ((signed long)expiry)) > 0;
}
inline void handleButtons() {
unsigned long now;
byte newButtonData[BUTTON_DATA_BUFFER_SIZE];
ZERO_ARRAY(newButtonData);
collectButtonDataFromInputs(newButtonData);
now = millis();
// Check on debounce timer
if (!ARRAYS_EQUAL(pendingButtonData, newButtonData)) {
// The new buffer contains fresh information.
// Set it to output after a debounce interval.
COPY_ARRAY(pendingButtonData, newButtonData);
debounceIsPending = true;
buttonDataDebounceExpiry = now + OUTPUT_DEBOUNCE_TIMEOUT;
}
else if (debounceIsPending && timeExpired(buttonDataDebounceExpiry, now)) {
// The debounce timer has expired.
// Copy the pending data to the last data, then expire the repeat timer.
debounceIsPending = false;
COPY_ARRAY(lastButtonData, pendingButtonData);
buttonDataRepeatExpiry = now - 1;
}
// Check on repeat timer
if (timeExpired(buttonDataRepeatExpiry, now)) {
// The timeout has expired, so output the current data and set a new timeout.
outputButtonDataAsSextets();
buttonDataRepeatExpiry = now + OUTPUT_UPDATE_TIMEOUT;
}
}
//
// Main program
//
void setup() {
byte i;
Serial.begin(115200);
for (i = 0; i < LIGHT_COUNT; ++i) {
// LEDs default to off.
byte pin = LIGHT_INFO[i].pinNumber;
pinMode(pin, OUTPUT);
digitalWrite(pin, false);
}
for (i = 0; i < BUTTON_COUNT; ++i) {
// Enable built-in pull-ups for inputs.
byte pin = BUTTON_INFO[i].pinNumber;
pinMode(pin, INPUT_PULLUP);
}
// Init application buffers.
resetLightData();
resetButtonData();
}
void loop() {
// Handle light data input
while (Serial.available() > 0) {
receiveLightData(Serial.read());
}
// Handle button data read and output
handleButtons();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment