LoRa, KPN en The Things Network met ATtiny85 en RN2483

Een Microchip RN2483 is een degelijke, door de LoRa Alliance gecertificeerde manier om verbinding te maken met een LoRaWAN gateway van KPN of The Things Network. En als het de bedoeling is om af en toe sensorwaarden naar het internet te sturen dan heb je niet veel extra’s nodig. De RN2483 is een SoC dat bestaat uit een Semtech SX1276 LoRa zendontvanger en een Microchip PIC18LF46K22 microcontroller. Deze laatste heeft mogelijkheden genoeg om een sensorwaarde in te lezen, maar om de certificering in stand te houden heeft Microchip ervoor gekozen het lastig te maken om de PIC18LF46K22 te laten herprogrammeren of aan te vullen met extra programmatuur. Maar geen paniek: met een Atmel ATtiny85 kan het ook. Totale kosten van zo’n setje: 20 euro.

In het schema hierboven is een RN2483 met enkele lijnen (GND, RX, TX, RESET) verbonden met de ATtiny85 microcontroller. Beide worden gevoed door een CR2016 knoopcel. Voor de gelegenheid is de RN2483 voorzien van een draadantenne van 8,2 cm, wat voldoende is voor een reikwijdte van een kilometer. De ATtiny85 maakt gebruik van pennen 1 en 3 om de RN2483 serieel aan te sturen en pen 7 om de RN2483 module te herstarten. In de sketch hieronder is het principe weergegeven:

#include 

#define rxPin 4
#define txPin 5
#define rn2483Reset 2

SoftwareSerial rn2483(rxPin, txPin);

byte crlf[2] = {0x0D,0x0A};         // Used to terminate RN2486 commands

void sendCommand(String cmd) {
  rn2483.print(cmd);
  rn2483.write(crlf, 2);
  delay(1000);
}

String getResponse() {
  String response = "";

  while (!rn2483.available()) { // Linger here while no response
  } // Might be better to create a timeout after a few seconds

  while (rn2483.available()) {
    response = response + char(rn2483.read());
  } 
  response = response + "";
  response.trim();
  return response;
}

int initLoRa() {
  String response;
  int code = -1;
  
  sendCommand("mac join otaa"); 
  response = getResponse();
  
  if (response == "ok") {
    code = 0;
    delay(6000);
    response = getResponse();
    if (response == "accepted") {
      // There. A valid LoRa connection
      code = 1;
    } else {
      // Denied. Either no free channels or something else
      code = 2;
    }
  } else {  // not ok
    // Not a wanted response. Something with the hardware
    // We might want to throw a panic here
    code = 3;
  }  
  return code;
}

void setup() {
  pinMode(rn2483Reset, OUTPUT);
  digitalWrite(rn2483Reset, false); // Reset
  delay(100);
  digitalWrite(rn2483Reset, true); // Not reset
  delay(2500);
  rn2483.begin(57600);
  while (!rn2483) {
    delay(100);
  }
  initLoRa();
}

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

}

Het solderen van montagedraden aan een RN2483 is nog niet zo eenvoudig, maar met een soldeerbout met een fijne punt moet het lukken. Het aansturen van de kick-ass PIC18LF46K22 door een ATtiny85 is zoiets als een Tesla model S laten starten met een accu van een lelijke eend, maar LoRaWAN is dan ook niet bedoeld voor iets omvangrijkers dan waar een ATtiny85 toe in staat is.

We staan op het Maak Festival!

Op zondag 12 april 2015 is er in Groningen het Maak Festival. Het Maak Festival is hét podium voor techniek, creativiteit en vindingrijkheid dat jaarlijks wordt georganiseerd op en rondom het Ebbingekwartierterrein. Wij staan er dit jaar met een tiental verschillende elektronische schakelingen opgebouwd rondom de ATtiny85, een kleine microcontroller met grote mogelijkheden. Voor onze stand hebben we dit jaar gekozen voor een eenvoudige webpagina, in plaats van grote banners. Vorig jaar zorgden deze grote banners ervoor dat we moesten uitleggen dat we hobbyisten waren, in plaats van een import-bedrijf in 3D printers.

IMG_3361

Uitlegkaart

Like-O-Meter

Like-O-Meter prototypesStel je voor, je loopt de zaal uit waar je net een presentatie hebt gevolgd en ziet op een tafeltje twee apparaten staan in het rood en groen. Beide hebben een verlichte knop aan de bovenkant, die uitnodigend pulserend verlicht is. Op het display zie je wat cijfers staan. Een poster aan de muur bevat de tekst ‘Evaluatie – Uw mening wordt op prijs gesteld’ en de kreet ‘Gebruik de Like-O-Meters hieronder’. Met een druk op de knop van de groene kolom kun je aangeven dat je de presentatie leuk vond, met een druk op de knop van de rode kolom geef je juist aan dat je het maar niks vond.

Dit is wat ik voor ogen heb met de elektronische telkolommen hiernaast. De techniek ervan is relatief simpel: een verlichte schakelaar aan de bovenkant, een display dat bij iedere druk op de knop één optelt en een stevige behuizing die ook bij intensief gebruik overeind blijft. Ik heb ze ‘Like-O-Meter’ gedoopt, een knipoog naar de ‘Like‘ knop die je van Facebook kent (‘Vind ik leuk’).

Het handgebouwde prototype van de Like-O-Meter is opgebouwd uit enkele PVC-delen van Martens, een Serieel LED display, een LED verlichte drukschakelaar en een ATtiny85. Het geheel is voorzien van een paar laagjes spuitlak van Motip.

IMG_1012

Aan de softwarekant moest ik wat improviseren om het geheel met een ATtiny85 aan de gang te krijgen. De ATtiny is een kleine 8-pins microcontroller, met 6 pennen vrij te gebruiken. Het LED display gebruikt hiervan twee (I2C clock en I2C data), de schakelaar gebruikt er één en de LED in de schakelaar ook één. Er zijn nog twee pennen vrij, voor bijvoorbeeld een RGB LED in de schakelaar.

Voor de I2C communicatie heb ik gebruik gemaakt van de TinyWireM library van Bro Hogan, die met 320 bytes een veel kleinere footprint heeft dan de AdaFruit LED Backpack library. De ATtiny85 heeft ruimte voor 8K gecompileerde programmatuur, hiervan gebruikt de Like-O-Meter iets meer dan 4K. De Arduino ontwikkelomgeving 1.01 heeft een fout die het gebruik van meer ruimte dan 4K voorkomt, dat was nog wel een uitzoekklusje. De broncode bevat aanwijzigingen voor het hercompileren en de benodigde codebibliotheken.

IMG_2474Het gedrag van de Like-O-Meter is als volgt. Zodra de 9 volt accu wordt ingeschakeld (de onderkant schroeft eenvoudig van de behuizing en de accu zit met een clipje vast) toont het display vier streepjes en wordt de LED in de schakelaar een seconde ontstoken. Hierna wordt een ‘0’ op het display getoond en is de LED in de schakelaar uit. Iedere keer als de schakelaar wordt ingedrukt brandt de LED en toont het display de tekst ‘PLUS’. Als de schakelaar weer wordt losgelaten toont het display opnieuw het getal, dat nu met één is verhoogd.

Na ongeveer 15 minuten inactiviteit wordt het display wat gedimd en pulseert de LED om de naderende slaapstand aan te kondigen. Een druk op de schakelaar levert het normale tel-er-een-bij-op gedrag en de slaapstand wordt onderbroken. Als er echter niet op de schakelaar wordt gedrukt dan gaat het display na een minuut uit. Het stroomverbruik is nu minimaal (ca. 7mA), maar de telstand wordt wel behouden. Als de schakelaar wordt ingedrukt dan wordt de tekst ‘AAN’ op het display getoond. Zodra de schakelaar nu wordt losgelaten dan toont het display de oude telstand en is de Like-O-Meter weer klaar voor gebruik. Om de telstand te wissen moet de accu worden losgekoppeld. Het stroomverbruik is sterk afhankelijk van het gebruik: met ‘8888’ op het display is de opgenomen stroom ongeveer 80mA. Bij regulier gebruik gaan de accu’s ongeveer 7 dagen mee.

/*
 * Like-O-Meter firmware 1.00
 * Copyright (c) 2013 Rudi Niemeijer
 * Placed in the Public Domain
 *
 * This is the firmware for the Like-O-Meter, a device that
 * acts as a counter with some light effects.
 *
 * A Like-O-Meter contains an Adafruit 4-digit 7-segment
 * LED backpack, button with built-in LED and an ATtiny85.
 *
 * In order to compile, this firmware requires the TinyWireM
 * library for I2C communication and the Tiny_LEDBackpack
 * library for the various LED display functions.
 *
 * TinyWireM library obtained from http://www.scenelight.nl/?wpfb_dl=22
 * Tiny_LEDBackpack obtained from https://github.com/millerlp/Tiny_LEDBackpack
 *
 * ATtiny pin configuration
 * Pin 1 - Not connected 
 * Pin 2 - Not connected (ATtiny PIN 3)
 * Pin 3 - Button (ATtiny PIN 4)
 * Pin 4 - GND
 * Pin 5 - SDA to LED backpack (grey)
 * Pin 6 - LED in button (ATtiny PIN 1 w. PWM)
 * Pin 7 - SCL to LED backpack (white)
 * Pin 8 - VCC/+5V
 *
 */

// These two libraries are essential
#include 
#include 

Tiny_7segment likeOMeterDisplay = Tiny_7segment();
int buttonPin = 4; // button that pulls the pin UP
int ledPin = 1; // indicator LED inside the button, pin 1 supports PWM
int buttonState; // current state of the button
int counterValue = 0; // current value for the counter

#define maxLEDBrightness 150 // maximum LED brightness 0..255
#define maxLEDBrightnessFade 75  // maximum LED brightness in fade mode
#define fadeSpeed 10 // increase LED brightness every fadeSpeed millis
#define napTimeOut 900000 // millis before sleep, 15 minutes
#define sleepTimeOut 960000 // millis before hybernation, 15 minutes + 60 seconds 

int currentLEDBrightness = 0;
int brighter = true;
unsigned long timeStampButtonPressed;
unsigned long timeStamp = 0;

#define i2c_addr 0x70 // stock address for Adafruit 7-segment LED backpack

void setup()
{
  pinMode(buttonPin, INPUT);
  pinMode(ledPin, OUTPUT);
  analogWrite(ledPin, maxLEDBrightness);
  likeOMeterDisplay.begin(i2c_addr); // initialize HT16K33 controller
  likeOMeterDisplay.clear(); // clear all digits on display
  likeOMeterDisplay.setBrightness(15); // set maximum display brightness
  likeOMeterDisplay.writeDigitRaw(0, 64);  // dash on position 0
  likeOMeterDisplay.writeDigitRaw(1, 64);  // dash on position 1
  likeOMeterDisplay.writeDigitRaw(3, 64);  // dash on position 3
  likeOMeterDisplay.writeDigitRaw(4, 64);  // dash on position 4
  likeOMeterDisplay.writeDisplay(); // push data to display
  delay(1000); // wait a second
  likeOMeterDisplay.clear(); // clear all digits on display
  displayInteger(counterValue);
  analogWrite(ledPin, 0); // disable the LED for visual verification
  timeStampButtonPressed = millis(); // simulate button pressed to prevent nap and sleep right from the beginning
}

void loop()
{
  buttonState = digitalRead(buttonPin);

  // NAP MODE TO ATTRACT ATTENTION
  if ((millis() > timeStampButtonPressed + napTimeOut) && (millis() > timeStamp + fadeSpeed))
  {
    timeStamp = millis();
    if (currentLEDBrightness == 0)
    {
      likeOMeterDisplay.setBrightness(0);
      likeOMeterDisplay.writeDisplay();
    }
    analogWrite(ledPin, currentLEDBrightness);
    if (brighter)
    {
      currentLEDBrightness++;
      if (currentLEDBrightness > maxLEDBrightnessFade)
      {
        currentLEDBrightness = maxLEDBrightnessFade;
        brighter = false;
      }
    } 
    else
    {
      currentLEDBrightness--;
      if (currentLEDBrightness < 0)
      {
        currentLEDBrightness = 0;
        brighter = true;
      }      
    }
  }
  
  // SLEEP MODE TO PREVENT BATTERY DEPLETION
  if ((currentLEDBrightness == 0) && (millis() > timeStampButtonPressed + sleepTimeOut)) // button not pressed for a very long time
  {
    // Sleep
    likeOMeterDisplay.clear(); // replace this with actual battery preserving code, like switching off
    likeOMeterDisplay.writeDisplay();
    digitalWrite(ledPin, 0);    
    while (!digitalRead(buttonPin)) // wait here until button is pressed
    {
    }
    // Wakeup
    likeOMeterDisplay.setBrightness(15);
    likeOMeterDisplay.writeDigitRaw(0, 119);  // A on position 0
    likeOMeterDisplay.writeDigitRaw(1, 119);  // A on position 1
    likeOMeterDisplay.writeDigitRaw(3, 55);  // N on position 3
    likeOMeterDisplay.writeDigitRaw(4, 0);  // blank on position 4
    likeOMeterDisplay.writeDisplay(); // push data to display
    delay(1000); // wait a second
    likeOMeterDisplay.clear(); // clear all digits on display
    displayInteger(counterValue);
    while (digitalRead(buttonPin)) // wait here until button is released
    {
    }
    timeStampButtonPressed = millis();
  }

  if (buttonState) // button is pressed
  {
    analogWrite(ledPin, maxLEDBrightness);
    likeOMeterDisplay.setBrightness(15);
    likeOMeterDisplay.writeDisplay();
    counterValue += 1;
    likeOMeterDisplay.clear();
    likeOMeterDisplay.writeDisplay(); 
    likeOMeterDisplay.writeDigitRaw(0,115); // P on position 0
    likeOMeterDisplay.writeDigitRaw(1,56); // L on position 1
    likeOMeterDisplay.writeDigitRaw(3,62); // U on position 3
    likeOMeterDisplay.writeDigitRaw(4,109); // S on position 4
    likeOMeterDisplay.writeDisplay();
    delay(250); // debounce time
    while (digitalRead(buttonPin)) // wait here until button is released
    {
    }
    delay(250); // debounce time
    displayInteger(counterValue);
    analogWrite(ledPin, 0);
    currentLEDBrightness = 0;
    brighter = true; // nap light is increasing first
    timeStampButtonPressed = millis(); // administer last time button pressed
  }
}

void displayInteger(int number)
{
  if (number == 0)
  {
      likeOMeterDisplay.writeDigitRaw(4, 63); // put a zero on the display
  } else
  {
    likeOMeterDisplay.print(number); // put the number on the display
  }
  likeOMeterDisplay.writeDisplay();
}

Automatisch fietsachterlicht

Het is weer herfst geworden. Een heerlijke tijd van het jaar, met een aantal traditionele evenementen zoals kastanjeszoeken en het controleren van de fietsverlichting. De achterlichten van onze beide kinderen zijn van het handmatige type en hoewel het veel goedkoper niet al te duur is om deze door een kant-en-klare automatische, zelfaanschakelende lamp te vervangen, ging me dat deze keer te ver.

LED fietsachterlichten
Een standaard fietsachterlicht met LED bestaat uit twee penlite-batterijen, een schakelaar, één of meer LED’s met wat weerstanden. De fietseigenaar schakelt de verlichting in met de schakelaar en schakelt deze na gebruik weer uit. Wat ik graag wil, is dat de fietseigenaar de fietsachterlicht wel inschakelt, maar dat dit achterlicht zelf de LED’s aanzet als dat nodig is. En het is pas nodig als er onvoldoende omgevingslicht is, én de fiets in beweging is. Ik heb enkele goedkope LED-fietsachterlichten gekocht. Hierin zitten 2 penlight-batterijen en 3 LED’s met elk een serieweerstand van 390 ohm. De 3 LED’s gebruiken gezamenlijk 70 mA.

Beetje Arduino, beetje Processing, beetje ISP
Uit de vorige jaren heb ik nog wat Arduino-spullen verzameld. Dit is allemaal te groot (en te duur) voor een fietsachterlicht, maar Atmel, de fabrikant van de microcontrollers die ook voor de Arduino’s worden gebruikt, heeft kleine chips op de markt die de ATtiny heten. Deze is er ondermeer in een 8-pins behuizing, met 5 universele I/O pennen, 8K aan flash-programmageheugen en 512 bytes werkgeheugen (de “ATtiny85”). En met een beetje moeite programmeerbaar met een Arduino, die zich voor die gelegenheid als “In Circuit Programmer” gedraagt. In dezelfde Processing taal als voor de Arduino geschikt is. En, belangrijk, de ATtiny85 is voor minder dan een euro te verkrijgen.

Sensoren
Om vast te stellen dat er onvoldoende omgevingslicht beschikbaar is, is een LDR prima geschikt. De ATtiny85 kan de weerstand hiervan inlezen met een analoge ingangspin. Een tiltschakelaar gedraagt zich als een soort bewegingssensor, prima te detecteren met een digitale ingangspin. En de LED’s kunnen, middels een transistor met open collector, met een digitale uitgangspin worden geschakeld (deze digitale pinnen kunnen maximaal 40 mA aansturen, nipt te weinig voor de benodigde 70 mA, vandaar de transistor). Kortom: het hele circuit gaat bestaan uit de volgende onderdelen:

  • ATtiny85 als regelneef
  • LDR voor het meting van omgevingslicht
  • Tiltschakelaar voor het bepalen van beweging
  • BC547 transistor voor het aansturen van de LED’s
  • Enkele weerstanden
  • De bestaande onderdelen: batterijen, schakelaar, LED’s
Het schema hieronder laat zien, dat de ATtiny85 het hart vormt van een schakeling waarin verder een tilt-schakelaar (horizontaal installeren zodat bij beweging van de fiets steeds het contact “stuitert”), een lichtgevoelige weerstand en een transistor zijn opgenomen. De LED’s en bijbehorende serieweerstanden, de batterijen en de schakelaar S1 zijn onderdeel van de bestaande achterlamp. Het geheel past op een stukje gaatjesprint van 3 x 3 centimeter.
Het leukst is natuurlijk, om hier een eigen stukje software voor te schrijven. Immers, dat opent de deur naar een persoonlijke noot. Ik heb ervoor gekozen om bij het aanschakelen van het achterlight de lamp even 3 keer te laten knipperen. Op die manier is het goed vast te stellen dat de elektronica het doet.
/*
 Bicycle automatic tail light
 Placed in the public domain
 Written by Rudi Niemeijer (10-2012)
*/

// Some constant values
#define ledPin 0 // Digital Output 0 (also PWM), AtTiny85 pin 5
#define ldrPin 3 // Analog Input 3, AtTiny85 pin 2 
#define vibPin 4 // Digital Input 1
#define hasNoMovementSensor true
#define milliSecondsToMeasure 1000
#define lightTreshold 10 // The higher the value, the sooner the LED switches on
#define movementTreshold 5 // At least this many diffs between 0 and 1 in milliSecondsToMeasure
#define milliSecondsToPauseInDarkBeforeParking 30000 // Stay on a while

// Define the four state the bicycle will be in
#define stateParked 0
#define stateMoveInLight 1
#define stateMoveInDark 2
#define statePauseInDark 3 // We stopped moving while the light was on

int i;
unsigned long t;
int inDaylight = true;
int hasMoved = false;
int curState = stateParked;
unsigned long sumLightValue;
int numMovementsLow;
int numMovementsHigh;
unsigned long timeSinceStartPauseInDark = 0;
int numMeasurements;

void flashLed(int numTimes) { // Flash the LED numTimes
 for (i = 0; i < numTimes; i++) {
   digitalWrite(ledPin, HIGH); // turn the LED on (HIGH is the voltage level)
   delay(200); // wait for a second
   digitalWrite(ledPin, LOW); // turn the LED off by making the voltage LOW
   delay(100); // wait for a second
 }
}

int readLdr() { // Read the LDR values, while switching temporary off the LED
 int analogValue;
 digitalWrite(ledPin, LOW);
 analogValue = analogRead(ldrPin);
 if (curState == stateMoveInDark) {
   digitalWrite(ledPin, HIGH);
 }
 return analogValue;
}

void burstLed() {
 for (i = 0; i < 25; i++) {
   digitalWrite(ledPin, HIGH);
   delay(25);
   digitalWrite(ledPin, LOW);
   delay(25);
 }
}

int difference(int a, int b) {
 if (a > b) {
   return a - b;
 } else {
   return b - a;
 }
}

void setup() { 
 pinMode(ledPin, OUTPUT);
 pinMode(vibPin, INPUT);
 flashLed(3); 
}

// the loop routine runs over and over again forever:
void loop() {
 switch (curState) {
   case stateParked:
     digitalWrite(ledPin, LOW);
     break;
   case stateMoveInLight:
     digitalWrite(ledPin, LOW);
     break;
   case stateMoveInDark:
     digitalWrite(ledPin, HIGH);
     break;
   case statePauseInDark:
     burstLed();
     break;
 }

 t = millis();
 sumLightValue = 0;
 numMovementsLow = 0;
 numMovementsHigh = 0;
 numMeasurements = 0;

 while (millis() - t < milliSecondsToMeasure) {
   sumLightValue = sumLightValue + readLdr();
   if (digitalRead(vibPin) == 0) {
     numMovementsLow = numMovementsLow + 1;
   } else {
     numMovementsHigh = numMovementsHigh + 1;
   }
   numMeasurements = numMeasurements + 1;
 }

 if ((difference(numMovementsHigh, numMovementsLow) > movementTreshold) || hasNoMovementSensor) {
   hasMoved = true;
 } else {
   hasMoved = false;
 }

 if (sumLightValue / numMeasurements < lightTreshold) {
   inDaylight = false;
 } else {
   inDaylight = true;
 }

 // Determine if curState has to change
 if (!hasMoved && inDaylight) {
   curState = stateParked;
 }
 if (hasMoved && inDaylight) {
   curState = stateMoveInLight;
 }
 if (hasMoved && !inDaylight) {
   curState = stateMoveInDark;
 }
 if (!hasMoved && !inDaylight && (curState == stateMoveInDark)) {
   timeSinceStartPauseInDark = millis();
   curState = statePauseInDark;
 }
 if (!hasMoved && !inDaylight && (curState == statePauseInDark) && (millis() - timeSinceStartPauseInDark) > milliSecondsToPauseInDarkBeforeParking) {
   curState = stateParked;
 }
}

Slotwoord
Het maken van een zelfschakelend achterlicht is ook voor een minder ervaren hobbyist eenvoudig genoeg te doen. Het is niet moeilijk. Maar het kost wat tijd om de ontwikkelstraat op poten te zetten. Als je eenmaal de smaak te pakken hebt om een ATtiny wat willekeurig eenvoudig regelwerk te laten doen, dan zijn de mogelijkheden onbeperkt.

Het achterlicht doet het ondertussen.