Benutzer-Werkzeuge

Webseiten-Werkzeuge


de:tech:gardenled

Dies ist eine alte Version des Dokuments!


Garten LED

Motivation

Unser Grundstück ist - wie häufig hier in Thailand - von einer zwei Meter hohen Mauer eingerahmt. Das hält einige Tiere wie streunende Strassenhunde aber auch Schlangen oder ähnliches etwas ab. Von innen kann man diese Wand natürlich begrünen.

Garden wall

Ein schicker Augenschmauss wäre eine indirekte Beleuchtung mit dimmbaren LED Spotlights hinter den Pflanzen. Dabei geht es um eine Länge von ca. 40m die mit Licht versorgt werden will. Eine Lösung muss her…

Lösungsansatz

Im Garten liegt eine 230V Versorgung die aber nicht schaltbar oder dimmbar ist, da andere Verbraucher wie Aussensteckdosen und das Bodenlicht daran angeschlossen sind. Eine Zusatzverdrahtung zum Haus habe ich ausgeschlossen nachdem ich versucht habe den Verlauf der unterirdischen Rohre zu verfolgen. Also muss etwas drahtloses her, das (wie immer) auch in Home Assistant einbindbar ist.

Vorab habe ich schon die LED Spots nach Aussehen, Leistung, Preis und Größe ausgewählt. Dabei fiel die Wahl auf einen 5W/600lm Strahler mit 12V AC/DC Versorgung im IP65 Gehäuse vom thailändischen Amazon: LAZADA.

Bei einem Preis von unter 4€ pro Stück kommt das ganze wirklich in einem wasserfesten Alugehäuse mit einer internen Steuerplatine und funktioniert! Lichtgeber ist eine 3mm LED die mit 21.5V bei einem Konstantstrom von 266mA betrieben wird. Die höhere Spannung wird durch einen Boost Regler (BP1808) erreicht, der auch einen ungenutzten DIM Eingang hat. Dieser kann analog oder über ein PWM Signal angesteuert werden. Ein erster Test mit einer 500Hz PWM zeigt das gewünschte Dimmverhalten.

LED driver front side LED driver back side

Jetzt brauchen wir nur ein Idee wie wir die im Home Assistant individuell gewählte Dimmung für jedes Spotlight als PWM Signal bereitstellen. Das Gehäuse der LED erlaubt dabei keine voluminöse Lösung. DMX512 erscheint hier etwas überdimensioniert. Also warum nicht auch mal einen proprietären Ansatz wagen.

Erste Versuche mit einer Kommunikation über Niedrigvoltleitungen schlugen aus Zuverlässigkeits- und Skalierungsgründen fehl. Dazu gibt es aber einen pfiffigen Ansatz: Simple Circuit Communicates Over Low-Voltage Power Lines. Das ganze mit einem ATTINY85 implementiert hat grundsätzlich mit zwei Transceiver über 10m bei 2400 Baud funktioniert.

Für Datenübertragungen über mehrere 10m kommt einem immer auch RS485 in Sinn. Passende Treiber-IC sind zuhauf verfügbar und eine Standard-UART reicht aus als Kommunikationsschnittstelle. Um es vorweg zunehmen, diese Idee hat dann auch zum Erfolg geführt. Hier die Beschreibung …

Hardware

Der LED Boost Driver mit dem BP1808 muss um eine Schaltung für den. Empfang von seriellen Daten über RS485 erweitert werden und das ganze möglichst klein. Der Innenraum des Lampengehäuses fasst nur ca. 40mm x 20mm. Als Steuer-CPU fiel die Wahl auf ein ATTINY85 von MicroChip den es im kleinen 8-pin SOIC Gehäuse gibt. Zudem verfügt er über ein EEPROM (z.B. für die Lampen ID), Oszillator on Chip und eine debugWIRE Schnittstelle. Die erlaubt in-place Debugging und vereinfacht so die Softwareentwicklung. Den RX485 Driver gibt es z.B. von MAXIM auch im 8-pin SOIC Gehäuse.

Schaltplan

Alles zusammengefasst sieht der Schaltplan so aus:

Der LED Boot Driver ist identisch zur Originalversion und nur durch die PWM-Ansteuerung ergänzt. Da ich bei den ersten Versuchen ein paar Hardwareausfälle (ATTINY85 + BP1808) wegen Überspannung hatte, habe ich das PWM Signal galvanisch mit einem Optokopller (OC1) getrennt. An der RS485 Schnittstelle sind auch optional die Abschlusswiderstände vorgesehen (R1,R3,R4). Der MAXIM Baustein Arbeit im Halb-Duplex, daher steuern wir die Richtung (Empfangen oder Senden) auch über den ATTINY85. Der letzte freie Ausgang an der COU steuert eine Status-LED (LED1). Die hilft beim Debugger und der Inbetriebnahme wenn man die POWER-LED abgeklemmt lässt.

PCB

Bei den geometrischen Vorgaben kommen wir um eine beidseitige Bestückung nicht herum. Aber auf der zweiten Seite befinden sich nur die vier SS14 Dioden (D3-D6) des Brückengleichrichters und der große Thru-hole Kondensator C. Diese Komponenten müssen dann handgelötet werden aber der Rest kann vorab im Reflow-Ofen (T-962) 'garen'.

Das Platinenmaß geht mit 39.3mm x 19mm schon an die mechanische Grenze aber es passt. In Realität sieht die Platine auf der Vorderseite doch schon ordentlich gepackt aus. Die handgeschriebene 4 auf der CPU ist die progammierte ID der LED. Über diese Nummer läßt sich das Spotlight einzeln ansprechen. Doch dazu später mehr.

PCB fron & back side

Firmware

Den physikalischen Layer haben wir mit RS485 gewählt aber wir brauchen noch einen Data Link Layer der die Fehlerfreiheit der Übertragung erhöht, schließlich sollen die Spotlights nicht flackern nur weil auf dem Bus gerade eine unerlaubte Signalparty stattfindet. Im Netz fand ich eine schlanke Lösung, die auch eine Python Version für den PC beinhaltet. Sie heißt MIN (Microcontroller Interconnect Network ). Der Payload läßt sich frei definieren und der Inhalt ist über eine 32-bit CRC abgesichert.

In meiner Implementierung besteht ein Frame aus 13 Bytes:

  • drei Header Bytes (0xAA)
  • des ID Bytes (0..15)
  • drei Bytes Payload
    • Function (0…255)
    • Value (0…255)
    • Delay (0…255)
  • vier CRC Bytes (32-bit)
  • ein EOF Byte (0x55)

der je nach Funktion vom Spotlight beantwortet wird. Bei Broadcastbefehlen erfolgt keine Antwort der Gegenseite. Hier die kurze Befehlsübersicht:

// min_id (8-bit):
// 0x00-0x0f 	ID LED lamp 1 - 16 -> function + value
// 0x10-0x1F	Response ID LED lamp 1 - 16
// 0x3E         all LED -> function + value (no response)
// 0X3F         -
//
// Payload / Response:
// Byte 1	Function
//  0x00	LED Off
//  0x01	LED Off (stored delay) 	
//  0x02	LED On
//  0x03	LED On (stored value/delay) 
//  0x04	Set stored value/delay
//  0x05	Get stored value/delay
//  0x06	Status    
//  0x07	Status LED on/off    
// Byte 2	Value	0..255 	LED brightness
// Byte 3	Delay	0..255 	LED fade up/down delay

// Payload Response Error:
// Byte 1	0xFF  
// Byte 2	Error code	
//  0x00	Payload size wrong (byte 3 = wrong size value))
//  0x01	Unknown Function
// Byte 3	<not used>

In den Spotlights lassen sich auch individuelle Helligkeits- und Fadewerte abspeichern, die dann bei Broadcast Befehlen unterschiedliche Helligkeiten ermöglichen. Im Module min.c ist das Protokoll implementiert. Zusätzlich benötigen wir noch eine Software UART Emulation, da der ATTINY85 keine H/W Variante besitzt. Die Baudrate ist auf 9600 Baud festgelegt damit keiner ins Schwitzen kommt, die Framelänge liegt so bei ca. 14ms. Mit Hilfe der kostenlosen IDE MPLAB und der kostenfreien Variante des XC8 C-Compilers von MicroChip läßt sich die S/W kompilieren und auf den Chip bannen.

Programmierung

In der Werkseinstellung des ATTINY85 ist das debugWIRE Interface ausgeschaltet und eine Programmierung nur über ISP möglich. Das bindet mehrere Pins des Prozessors und geht nicht on-board. Daher sollte man sich einen kleinen Programmieradapter (Sockel) für das Package SOIC8 (200mil) zulegen und die Fuses vor dem Bestücken damit programmieren. Danach läßt sich der Software auch über das 1-pin Interface dWire programmieren.

SOIC8 Adapter

Das Debug- und Programmingtool der ersten Wahl ist und bleibt SNAP (PG164100). Alle diese Tools sind im Online Markt oder der Bucht einfach und für den DIY-Geldbeutel erschwinglich zu finden.

 SNAP (PG164100) Programer

Integration

Hat man die Spotlights wie oben beschrieben programmiert, können die LEDs schon über einen USB-RS485 Wandler mit dem PC angesprochen werden. Für das Protokoll steht auch eine Pythonvariante zur Verfügung.

Mein Wunsch es aber die Spotlights in Home Assistant (HA) zu integrieren. Dazu brauchen wir ein Gateway dass die Spotlights als Lights in HA abbildet und zusätzlich einen UART Server damit wir auch beliebige Befehle an die LEDs senden können. Klingt kompliziert ist es aber mit ESPHome nicht.

Für den UART server gibt es schon hier eine schicke Lösung im Netz. Den Code müssen wir nur leicht anpassen, in dem wir vor und nach dem Senden die Richtung des RS485 Driver anpassen. Die Zeilen mit ### wurden in die Senderoutine zusätzlich eingebaut. Der gesamte veränderte Code ist im Downloadbereich zu finden.

void StreamServerComponent::write() {
    digitalWrite(DIR_PORT, HIGH);   // ### set direction to receive
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2021, 10, 0)
    this->stream_->write_array(this->recv_buf_);
    this->recv_buf_.clear();
#else
    size_t len;
    while ((len = this->recv_buf_.size()) > 0) {
        this->stream_->write(this->recv_buf_.data(), len);
        this->recv_buf_.erase(this->recv_buf_.begin(), this->recv_buf_.begin() + len);
    }
#endif
    this->stream_->flush();         // ### Added by DIRB
    digitalWrite(DIR_PORT, LOW);    // ### set direction to receive
}

Für die Einbindung der LEDs als Lights schreiben wir wieder ein custom component. Das folgende C-Program garden-leds.h stellt einen Float Output für Home Assistant zur Verfügung und beinhaltet eine sehr schlanke Codierung des MIN Protokolls.

garden-less.h
#include "esphome.h"
 
using namespace esphome;
 
#define LED_GLOBAL          0x3E        // all LEDs
#define LED_ON              0x02        
#define LED_FADE            0x00        // no fade
#define LED_BRIGHTNESS      0x00        // off
 
#define DIR_PORT            13
#define BAUDRATE            9600
 
#define MAX_BYTES_PER_FRAME 	16
#define MAX_PAYLOAD		3
 
uint8_t min_payload[3] = { LED_ON, LED_BRIGHTNESS, LED_FADE }; 
 
// MIN Protocol v2.0 Copyright (c) 2014-2017 JK Energy Ltd.
// Use authorized under the MIT license.
 
// Special protocol bytes
enum {
    HEADER_BYTE = 0xaaU,
    STUFF_BYTE = 0x55U,
    EOF_BYTE = 0x55U,
};
 
// Number of bytes needed for a frame with a given payload length, excluding stuff bytes
// 3 header bytes, ID/control byte, length byte, seq byte, 4 byte CRC, EOF byte
#define ON_WIRE_SIZE(p)                             ((p) + 11U)
 
struct crc32_context {
    uint32_t crc;
};
 
struct min_context {
    uint8_t rx_frame_payload_buf[MAX_PAYLOAD];      // Payload received so far
    uint32_t rx_frame_checksum;                     // Checksum received over the wire
    struct crc32_context rx_checksum;               // Calculated checksum for receiving frame
    struct crc32_context tx_checksum;               // Calculated checksum for sending frame
    uint8_t rx_header_bytes_seen;                   // Countdown of header bytes to reset state
    uint8_t rx_frame_state;                         // State of receiver
    uint8_t rx_frame_payload_bytes;                 // Length of payload received so far
    uint8_t rx_frame_id_control;                    // ID and control bit of frame being received
    uint8_t rx_frame_seq;                           // Sequence number of frame being received
    uint8_t rx_frame_length;                        // Length of frame
    uint8_t rx_control;                             // Control byte
    uint8_t tx_header_byte_countdown;               // Count out the header bytes
    uint8_t port;                                   // Number of the port associated with the context
};
 
// MIN calback functions
// ---------------------------------------------------------------------------
void min_tx_start(uint8_t port) {
    digitalWrite(DIR_PORT, HIGH);     // switch RS485 driver to send mode
}
 
// ---------------------------------------------------------------------------
void min_tx_finished(uint8_t port) {
    Serial.flush();
    digitalWrite(DIR_PORT, LOW);      // switch RS485 driver to send mode
}
 
// ---------------------------------------------------------------------------
// Tell MIN how much space there is to write to the serial port. This is used
// inside MIN to decide whether to bother sending a frame or not.
uint16_t min_tx_space(uint8_t port)
{
    return MAX_BYTES_PER_FRAME;
}
 
// ---------------------------------------------------------------------------
// Send a character on the designated port.
void min_tx_byte(uint8_t port, uint8_t byte)
{
  // Ignore 'port' because we have just one context.
    Serial.write(byte);  
}
 
// ---------------------------------------------------------------------------
static void crc32_init_context(struct crc32_context *context)
{
    context->crc = 0xffffffffU;
}
 
// ---------------------------------------------------------------------------
static void crc32_step(struct crc32_context *context, uint8_t byte)
{
    uint32_t j;
    context->crc ^= byte;
    for(j = 0; j < 8; j++) {
        uint32_t mask = (uint32_t) -(context->crc & 1U);
        context->crc = (context->crc >> 1) ^ (0xedb88320U & mask);
    }
}
 
// ---------------------------------------------------------------------------
static uint32_t crc32_finalize(struct crc32_context *context)
{
    return ~context->crc;
}
 
// ---------------------------------------------------------------------------
static void stuffed_tx_byte(struct min_context *self, uint8_t byte, bool crc)
{
    // Transmit the byte
    min_tx_byte(self->port, byte);
    if(crc) {
        crc32_step(&self->tx_checksum, byte);
    }
 
    // See if an additional stuff byte is needed
    if(byte == HEADER_BYTE) {
        if(--self->tx_header_byte_countdown == 0) {
            min_tx_byte(self->port, STUFF_BYTE);        // Stuff byte
            self->tx_header_byte_countdown = 2U;
        }
    }
    else {
        self->tx_header_byte_countdown = 2U;
    }
}
 
// ---------------------------------------------------------------------------
// Send frame on wire
static void on_wire_bytes(struct min_context *self, uint8_t id_control, uint8_t seq, uint8_t const *payload_base, uint16_t payload_offset, uint16_t payload_mask, uint8_t payload_len)
{
    uint8_t n, i;
    uint32_t checksum;
 
    self->tx_header_byte_countdown = 2U;
    crc32_init_context(&self->tx_checksum);
 
    min_tx_start(self->port);
 
    // Header is 3 bytes; because unstuffed will reset receiver immediately
    min_tx_byte(self->port, HEADER_BYTE);
    min_tx_byte(self->port, HEADER_BYTE);
    min_tx_byte(self->port, HEADER_BYTE);
 
    stuffed_tx_byte(self, id_control, true);
    if(id_control & 0x80U) {
        // Send the sequence number if it is a transport frame
        stuffed_tx_byte(self, seq, true);
    }
 
    stuffed_tx_byte(self, payload_len, true);
 
    for(i = 0, n = payload_len; n > 0; n--, i++) {
        stuffed_tx_byte(self, payload_base[payload_offset], true);
        payload_offset++;
        payload_offset &= payload_mask;
    }
 
    checksum = crc32_finalize(&self->tx_checksum);
 
    // Network order is big-endian. A decent C compiler will spot that this
    // is extracting bytes and will use efficient instructions.
    stuffed_tx_byte(self, (uint8_t)((checksum >> 24) & 0xffU), false);
    stuffed_tx_byte(self, (uint8_t)((checksum >> 16) & 0xffU), false);
    stuffed_tx_byte(self, (uint8_t)((checksum >> 8) & 0xffU), false);
    stuffed_tx_byte(self, (uint8_t)((checksum >> 0) & 0xffU), false);
 
    // Ensure end-of-frame doesn't contain 0xaa and confuse search for start-of-frame
    min_tx_byte(self->port, EOF_BYTE);
 
    min_tx_finished(self->port);
}
 
// ---------------------------------------------------------------------------
// Sends an application MIN frame on the wire (do not put into the transport queue)
void min_send_frame(struct min_context *self, uint8_t min_id, uint8_t const *payload, uint8_t payload_len)
{
    if((ON_WIRE_SIZE(payload_len) <= min_tx_space(self->port))) {
        on_wire_bytes(self, min_id & (uint8_t) 0x3fU, 0, payload, 0, 0xffffU, payload_len);
    }
}
 
struct min_context min_ctx;         // min protocol context
 
// ---------------------------------------------------------------------------
// float output class
class Min_Led : public Component, public FloatOutput {
 public:
    void setup() override {
        // switch RS485 driver to receive mode
        digitalWrite(DIR_PORT, LOW);
        Serial.begin(BAUDRATE);
    }
 
    void write_state(float state) override {
        // state is the amount this output should be on, from 0.0 to 1.0
        // we need to convert it to an integer first
        int value = state * 255;
        min_payload[1] = value;
        min_send_frame(&min_ctx, LED_GLOBAL, min_payload, MAX_PAYLOAD);
    }
};

Nun legen wir wieder ein neues Device in ESPHome an und verweisen auf die beiden Custom Codes stream-server und garden-leds.h. Den ersten Part haben wir vorher in config/esphome/my_components/stream-server und das zweite File in config/esphome kopiert.

esphome:
  name: garden-leds
  includes:
    - garden-leds.h

external_components:
  - source:
      type: local
      path: my_components
    components: [stream_server]  

Zu guter Letzt das Setup des Light und des Servers:

uart:
  id: uart_min
  tx_pin: GPIO1
  rx_pin: GPIO3
  baud_rate: 9600

stream_server:
  uart_id: uart_min
  port: 6638

output:
  - platform: custom
    type: float
    lambda: |-
      auto min_led_pwm = new Min_Led();
      App.register_component(min_led_pwm);
      return{min_led_pwm};
    outputs:
      id: led_pwm

light:
  - platform: monochromatic
    name: "Garden Wall"
    output: led_pwm

Hardware Gateway

An das ESP Modul der Wahl muss nun vor der Inbetriebnahme nur noch an ein RS485 Wandler angeschlossen werden. Das gibt es auch wieder fertig in der Bucht.

Die Verdrahtung ist einfach.

Downloads

Spenden

Wenn ihr meine Arbeit unterstützen wollt, so könnt ihr mir gerne einen Cappuccino oder so spenden: .

de/tech/gardenled.1668364381.txt.gz · Zuletzt geändert: 2022/11/13 18:33 von bullar