Mit dem Node.js Modul pcf8574 ist es möglich, jeden Pin eines PCF8574/PCF8574A Porterweiterungs-ICs einzeln zu kontrollieren. Es ist eine Eigenentwicklung von mir und kann über den Node.js Paketmanager npm
installiert werden.
Der Quellcode ist öffentlich auf GitHub verfügbar. Hier können auch Fehler und Ideen gemeldet werden.
Was kann das Modul pcf8574?
Jeder Pin eine PCF8574/PCF8574A ICs kann einzeln entweder als Eingang oder als Ausgang definiert werden. Änderungen an Eingangs-Pins können über einen Interrupt oder durch aktives Polling erfasst werden.
Der PCF8574/PCF8574A ist ein 8-Bit Porterweiterungs-IC, welcher über den I²C-Bus angesteuert wird. Jeder der 8 Pins kann separat als Eingang oder Ausgang benutzt werden. Weiterhin bietet der IC ein Interrupt-Signal, welches dem I²C-Master (z.B. einem Raspberry Pi) mitteilen kann, dass sich etwas an einem Pin geändert hat. Weitere Informationen zu dem PCF8574/PCF8574A sind im Datenblatt von Texas Instruments zu finden.
Installation
npm install pcf8574
TypeScript-Definitionen sind in dem Paket enthalten. Somit kann es ohne Weiteres direkt mit TypeScript verwendet werden.
Das Modul sollte auf jedem Linux basierten Betriebssystem funktionieren, sofern eine I²C Schnittstelle vorhanden ist.
Zur Verwendung der Interrupt-Erkennung ist ein Raspberry Pi (oder ähnliches) erforderlich.
Beispiele
Beachte bitte, dass das i2c-bus
Objekt vorher erstellt und zusammen mit der I²C-Adresse des ICs an den Konstruktor der PCF8574-Klasse übergeben werden muss.
Das folgende Beispiel ist auch zusammen mit einem TypeScript-Beispiel im Repository unter examples zu finden.
// Einbinden des pcf8574 Moduls
const PCF8574 = require('pcf8574').PCF8574;
// Oder importieren nach ES6-Style
// import { PCF8574 } from 'pcf8574';
// Einbinden des i2c-bus Moduls und öffnen des Bus
const i2cBus = require('i2c-bus').openSync(1);
// Die Adresse des PCF8574/PCF8574A festlegen
const addr = 0x38;
// Initialisieren eines neuen PCF8574 mit allen Pins auf high-Level
// Anstelle von 'true' kann auch eine 8-Bit Bitmaske verwendet werden,
// um jeden Pin einzeln festzulegen (z.B. 0b00101010)
const pcf = new PCF8574(i2cBus, addr, true);
// Aktiviere die Interrupt-Erkennung auf BCM-Pin 17 (dies ist GPIO.0)
pcf.enableInterrupt(17);
// Alternativ kann auch beispielsweise ein Intervall verwendet werden,
// um manuell alle 250ms Änderungen abzufragen
// setInterval(pcf.doPoll.bind(pcf), 250);
// Beachte das Fehlende ; am Ende der folgenden Zeilen.
// Die ist eine Promise-Kette!
// Definiere Pin 0 des ICs als invertierten Ausgang mit dem Anfangswert false
pcf.outputPin(0, true, false)
// Dann definiere Pin 1 als invertierten Ausgang mit dem Anfangswert true
.then(() => {
return pcf.outputPin(1, true, true);
})
// Dann definiere Pin 7 als nicht invertierten Eingang
.then(() => {
return pcf.inputPin(7, false);
})
// Warte eine Sekunde
.then(() => new Promise((resolve) => {
setTimeout(resolve, 1000);
}))
// Dann schalte Pin 0 ein
.then(() => {
console.log('turn pin 0 on');
return pcf.setPin(0, true);
})
// Warte eine Sekunde
.then(() => new Promise((resolve) => {
setTimeout(resolve, 1000);
}))
// Dann schalte Pin 0 aus
.then(() => {
console.log('turn pin 0 off');
return pcf.setPin(0, false);
});
// Fügen einen Event-Listener dem 'input'-Event hinzu
pcf.on('input', (data) => {
console.log('input', data);
// Prüfe ob ein Taster, der mit Pin 7 verbunden ist,
// gedrückt wurde (Signal am Pin geht auf low)
if(data.pin === 7 && data.value === false){
// Pin 1 toggeln
pcf.setPin(1);
}
});
// Handler zum Aufräumen bei einem SIGINT (ctrl+c)
process.on('SIGINT', () => {
pcf.removeAllListeners();
pcf.disableInterrupt();
});
API
Die API verwendet Events für erkannte Änderungen an Eingängen und Promises für alle asynchronen Aktionen.
Änderungen an Eingängen können auf zwei Wege erkannt werden:
- Unter Verwendung eines GPIO zur Beachtung des Interrupt-Signal vom PCF8574/PCF8574A. Empfohlen bei Verwendung eines Raspberry Pi oder ähnlichem.
- Manueller Aufruf der Funktion
doPoll()
in regelmäßigen Abständen, um aktiv den aktuellen Status des ICs abzufragen. Dies führt zu einer höheren Last auf dem I²C-Bus.
Wenn ein Pin als Eingang definiert ist und eine Änderung an diesem Pin erkannt wird, dann wir ein input
-Event ausgelöst. Diesem Event wird ein Objekt mit dem Pin (pin
) und dem neuen Wert (value
) mitgegeben.
Für jeden Pin kann das invertiert-Flag einzeln gesetzt werden, was einen invertierten Eingang oder Ausgang zur Folge hat. Wenn ein invertierter Eingang ein low-Level aufweist wird dies als true interpretiert und ein high-Level als false. Ein invertierter Ausgang wird ein low-Level aufweisen, wenn er auf true gesetzt wird und ein high-Level bei false.
new PCF8574(i2cBus, address, initialState)
constructor (i2cBus: I2CBus, address: number, initialState: boolean | number);
Konstruktor für eine neue PCF8574-Instanz.
i2cBus
– Instanz eines geöffneten i2c-bus.address
– Die Adresse des PCF8574/PCF8574A ICs.initialState
– Der Anfangszustand der Pins des ICs. Es kann eine Bitmaske (z.B. 0b00101010) verwendet werden, um jeden Pin einzeln festzulegen, oder true/false, um alle Pins auf einmal zu definieren.
Beachte bitte, dass das i2c-bus
Objekt vorher erstellt und an den Konstruktor der PCF8574-Klasse übergeben werden muss.
Wenn der IC mit einem oder mehreren Eingängen verwendet wird, dann muss eine der folgenden Funktionen aufgerufen werden:
enableInterrupt(gpioPin)
, um die Interrupt-Erkennung über einen GPIO-Pin zu aktivieren, oderdoPoll()
in regelmäßigen Abständen, um Eingangsänderungen durch manuelles Abfragen zu erkennen.
enableInterrupt(gpioPin)
<code>enableInterrupt (gpioPin: PCF8574.PinNumber): void;
Aktiviert die Interrupt-Erkennung am angegebenen GPIO-Pin. Ein GPIO-Pin kann für mehrere Instanzen der PCF8574-Klasse verwendet werden.
gpioPin
– BCM-Nummer des Pins, der für Interrupt-Erkennung des PCF8574/PCF8574A ICs genutzt wird.
disableInterrupt()
disableInterrupt (): void;
Deaktiviert die Interrupt-Erkennung. Gibt außerdem dem GPIO-Pin wieder frei.
doPoll()
doPoll (): Promise<void>;
Manuell Änderungen an Eingangs-Pins des PCF8574/PCF8574A abfragen.
Wenn eine Änderung erkannt wurde, dann wird das input
-Event ausgelöst. Diesem Event wird ein Objekt mit dem Pin (pin
) und dem neuen Wert (value
) des Pins mitgegeben.
Diese Funktion muss in regelmäßigen Abständen aufgerufen werden, wenn keine Interrupt-Erkennung verwendet wird. Erfolgt ein neuer Poll bevor der letzte abgeschlossen ist, dann wird das Promise mit einem Fehler zurückgewiesen.
outputPin(pin, inverted, initialValue)
outputPin (pin: PCF8574.PinNumber, inverted: boolean, initialValue?: boolean): Promise<void>;
Definiert einen Pin als Ausgang.
pin
– Die Nummer des Pins. (0 bis 7)inverted
– true wenn der Pin invertiert sein soll.initialValue
– (optional) Der Anfangswert des Pins. Wird beim Einrichten des Pins gesetzt.
inputPin(pin, inverted)
inputPin (pin: PCF8574.PinNumber, inverted: boolean): Promise<void>;
Definiert einen Pin als Eingang. Dies markiert einen Pin für die Auswertung von Signaländerungen und setzt einen high-Pegel an diesem Pin.
pin
– Die Nummer des Pins. (0 bis 7)inverted
– true wenn der Pin invertiert sein soll.
Achtung: Der Pin wird immer intern auf einen high-Pegel gesetzt. (Pull-Up)
setPin(pin, value)
setPin (pin: PCF8574.PinNumber, value?: boolean): Promise<void>;
Setzt den Wert eines Ausgangs. Wenn kein neuer Wert angegeben wird, dann wird der Ausgang getoggelt.
pin
– Die Nummer des Pins. (0 bis 7)value
– Der neue Wert für den Pin.
setAllPins(value)
setAllPins (value: boolean): Promise<void>;
Setzt den Wert aller Ausgänge.
value
– Der neue Wert für alle Ausgangs-Pins.
getPinValue(pin)
getPinValue (pin: PCF8574.PinNumber): boolean;
Gibt den aktuellen Wert eines Pins zurück. Dies ist der zuletzt gespeicherte Zustand des Pins und nicht zwangsläufig der aktuell am IC anliegende Zustand. Um den aktuellen Zustand zu erhalten muss zuerst doPoll()
aufgerufen werden, wenn keine Interrupts verwendet werden.
Lizenz
Lizenziert unter der GPL Version 2
Copyright ©2017-2020 Peter Müller
29. Mai. 2017 um 17:05
Hallo,
Wir haben mehrere PCF8574 und PCF8574A hintereinander mit einem durchverbundenen Interrupt. Geht das mit diesem NodeJS Modul auch, oder ist das nur für einem.
Vielen Dank,
MfG
Reno
29. Mai. 2017 um 20:33
Hallo Reno,
ich habe eben die Handhabung der GPIOs etwas angepasst.
Mehrere Instanzen der PCF8574 Klasse teilen sich nun ein GPIO-Objekt, wenn enableInterrupt() mit der gleichen Pin-Nummer aufgerufen wird.
Damit ist es nun auf jeden Fall möglich, den gleichen Interrupt für mehrere ICs zu nutzen. Einfach enableInterrupt() mit der gleichen GPIO Pin-Nummer aufrufen.
Die neue Version 1.1.0 ist jetzt über npm verfügbar.
30. Mai. 2017 um 11:09
Hallo Peter,
Einen Riesendank für Ihre rasche Antwort und Lösung!
Um sicher zu sein dass ich es gut verstanden hab, hier ein kleines Vorbild wie ich es verwenden will:
Stimmt das?
MfG
Reno
30. Mai. 2017 um 12:03
Sieht soweit gut aus.
Die Input Pins sollten jedoch besser nacheinander über eine Promise-Chain gesetzt werden:
Wenn alle ICs gleich verwendet werden (mit 8 Inputs), dann ließe sich der Code auch wie folgt deutlich verkürzen. Dabei mit Hilfe von zwei schleifen die gleiche Promise-Chain wie im oberen Beispiel erzeugt.
30. Mai. 2017 um 14:07
Sie sind einfach großartig!
Vielen Dank!
30. Mai. 2017 um 14:18
Ich habe noch eine letzte kurze Frage.
Die Modulen welche ich benütze sind Optocoupler Input Modulen woran Pushbuttons hängen. https://www.ereshop.com/shop/free/I2C-IN830M_SHEET.pdf
Das heisst dass wir einen kurzen Impuls bekommen. Wenn ich aber Ihre Code sehe in der _poll Funktion, dann wundere ich mich ob ich dann zweifach in den ‘input’ Event komme?
Vielen Dank noch mal!
30. Mai. 2017 um 20:39
Das input-Event wird bei jeder Änderung an einem Pin ausgelöst. Der kurze Impuls eines Buttons löst somit zwei Events aus. Einmal mit
value: false
und einmal mitvalue: true
, oder umgekehrt.Der PCF8574 zieht bei jeder Änderung an einem seiner Pins den Interrupt-Pin auf ein Low-Level. Dieses fallende Flanke wird über den GPIO vom Raspberry Pi erkannt und es wird die interne Funktion
_poll()
aufgerufen, welche den aktuellen Zustand der Pins am PCF8574 ließt und auswertet. Sobald vom PCF8574 nach dem Interrupt gelesen wurde, gibt dieser seinen Interrupt-Pin wieder frei, wodurch dann wieder ein High-Level anliegt und der nächste Interrupt erfolgen kann.Bei Verwendung des Interrupts werden auch sehr kurze Impulse an einem der Pins des PCF8574 erkannt und die entsprechenden Events ausgelöst. Bei manuellem Polling über die
doPoll()
Funktion hingegen kann es vorkommen, dass sehr kurze Impulse nicht erkannt werden.30. Mai. 2017 um 21:21
Super, vielen Dank!
3. Jun. 2017 um 22:40
Hallo Peter,
Nach Testen usw. und auch noch welche Fehler raus geholt zu haben, scheint alles super zu funktionieren.
Hier meine Code:
MfG
Reno
3. Jun. 2017 um 22:55
=)
2. Apr. 2018 um 19:11
Hallo Peter,
ich benutze bei der SmartHome Lösung “pimatic” dein Plugin “pimatic-pcf8574” um einen PortExpander anzusprechen. Leider habe ich damit ein paar Probleme. Zum einen bekomme ich sehr oft den Fehler “[pimatic-pcf8574] warn: portexpander Error while polling: An other poll is in progress”
Ich benutze die Einstellung “inputChangeDetection = polling” und “pollingInterval = 200”.
Kann ich irgendwie erkennen welches andere Plugin stört?
Außerdem habe ich immer beim Neustart von pimatic ein oder zwei Pins, die das Plugin nicht zuweisen kann (z.B. [pimatic-pcf8574] error: Error setting pin as input (device: port-1-in, pcf8574ic: portexpander, pin: 1))
Wenn ich dann das Device lösche und neu einrichte funktioniert es. Die Pins sind aber auch nicht immer die gleichen?!
MfG
Hermann
3. Apr. 2018 um 19:55
Hallo Hermann,
der Fehler `Error while polling: An other poll is in progress` kommt, wenn ein erneuter Poll gestartet wird, bevor der vorherige abgeschlossen ist. Grund dafür sind entweder Probleme auf dem Bus oder viele Geräte am Bus mit einem zu kurzen `pollingInterval`.
Der zweite Fehler mit den Pins deutet auch wieder auf Fehler auf dem Bus hin. Immer wenn ein Pin als Eingang definiert wird, dann wird der Pin am PCF8574 auf ein high-Signal gesetzt. Wenn dieser Befehl nicht richtig an den IC übertragen werden kann, dann kommt der Fehler.
Wie viele PCF8574 ICs hast du am I2C-Bus? Noch andere ICs und/oder Pimatic Plugins, die den I2C-Bus nutzen?
4. Apr. 2018 um 20:43
Hallo Peter,
erstmal Danke für deine schnelle Antwort.
Am I2C Bus habe ich im Moment eigentlich nur ein PCF8574 IC angeschlossen. Das einzige andere Plugin, welches noch ein Bus System nutzt sind die vier DS18B20 Sensoren mit dem entsprechenden Plugin. Das sollte aber kein I2C sein.
Ich hatte zuerst noch die Länge des I2C Busses in Verdacht (ca. 10m abgeschirmtes Kabel), aber auch auf einem Steckbrett direkt am RasPi (5cm) tritt der Fehler auf.
Ich werde mal versuchen den Polling Intervall auf eine längere Zeit einstellen.
MfG
Hermann
5. Apr. 2018 um 20:15
Bei einem einzelnen PC8574 am I2C-Bus sollte es (zumindest bei kurzen Leitungen) keine Probleme geben. Hast du vielleicht zusätzliche Pull-Up Widerstände an den beiden Bus-Leitungen? Der RasPi hat intern schon je 1,8 kΩ als Pull-Up verbaut, wodurch zusätzliche Widerständen den Bus stören könnten.
Die bessere Alternative zum Polling ist immer, sofern möglich, die Interrupt-Leitung des PCF8574 zu verwenden. Das reduziert die Aktivität auf dem Bus erheblich, da nur dann die Pins des ICs abgefragt werden, wenn sich wirklich etwas geändert hat.
Dazu musst du lediglich den INT-Pin des PCF8574 mit einem freien GPIO des RasPi verbinden und noch einen 10 kΩ Pull-Up von +3,3V auf diese Leitung einbauen. In den Einstellungen des Pimatic-Devices dann `inputChangeDetection` auf `interrupt` stellen und bei `interruptPin` den entsprechen Pin eintragen.
9. Apr. 2018 um 21:38
Hallo,
die beiden Busleitungen gehen direkt vom 40Pin Header ab und dann auf die PCF8574 Platine (keine Widerstände). Ich habe die Pollingfrequenz jetzt auf 2 Sekunden gestellt, mit dem Erfolg, dass jetzt nur noch ganz selten Pollingfehler auftreten.
Die Variante mit den dem Polling habe ich deswegen gewählt, da ich nur noch die zwei I2C Leitungen an meinem Raspi (pimatic) frei hatte und noch mehr Sensoren abfragen wollte.
Zu der Interrupt Variante eine Verständnisfrage:
Ich benutze den PCF8574 als zusätzliche Eingänge, woher weiß der Raspi mit seinem GPIO, wann er die Pins des ICs abfragen muss?
Außerdem habe ich noch einen seltsamen Fehler festgestellt: ich habe einen Port als Eingang und in pimatic als PresenceSensor definiert. In der pimatic GUI ist der Sensor IMMER aktiv (auch wenn sich keiner vor dem angeschlossenen PIR bewegt). Rufe ich aber in pimatic den Graphen zu dem Sensor auf, sehe ich dort meine Bewegungen und auch die entsprechenden Regeln werden korrekt ausgeführt.
11. Apr. 2018 um 12:42
Das ist seltsam, bei einem so langen Intervall sollte es eigentlich absolut keine Probleme beim Abfragen geben. Kannst du über ein Terminal mit `i2cget -y 1 0x20` den IC mehrfach schnell hintereinander abrufen? `0x20` ist dabei die Adresse deines PCF8574. Als Antwort sollte beispielsweise `0xff` angezeigt werden. Eventuell musst du vorher noch mittels `sudo apt install i2c-tools` die I2C-Tools installieren.
Der Interrupt-Pin des PCF8574 wird bei jeder Änderung an einem IO-Pin vom IC aus auf LOW gesetzt, bis vom IC gelesen wurde. Dieses LOW-Signal erkennt der Raspi über die zusätzliche Interrupt-Leitung an seinem GPIO und fragt dann IC ab. Damit können Änderungen an den Pins des ICs deutlich schneller und zuverlässiger erkannt werden, als mit dem Polling.
Tritt das Problem mit dem PresenceSensor nur in Verbindung mit dem PCF8574 auf, oder auch wenn du den PIR direkt an einen GPIO am Raspi anschließt?
14. Apr. 2018 um 21:41
So, da bin ich wieder…
bei der schnellen, mehrfachen Eingabe von “i2cget -y 1 0x20” bekomme ich als Antwort “0xfe”. Auch wenn ich 2-3x pro Sekunde den Befehl eingebe, kommt keine Fehlermeldung sondern immer nur das “0xfe”.
Ich muss mal schauen, ob ich mein pimatic Hardware so ändern kann, dass ich den Interrupt PIN doch nutzen kann und so das Problem bei der Abfrage umgehen kann.
Das Problem mit dem PIR habe ich wohl nicht richtig erkannt. Bei genauerer Betrachtung musste ich festgestellen, dass das Signal invertiert ist, und deswegen fast immer aktiv anzeigte. Leider habe ich in der GUI keine Änderung, egal ob ich im Plugin inverted auf true oder false setze.
11. Mrz. 2020 um 14:20
Hallo
gibt es Pläne, das Modul noch zu erweiteren? Es gibt zum Beispiel von Horter & Kalb eine Analoge Ausgangskarte (0-10V; LINK: https://www.horter.de/blog/i2c-analog-output-4-kanaele-10-bit/) welche wie ein PFC8574 angesteuert werden kann.
Leider kann ihre Library aber keine Analogkarten ansteuern und sonst gibt es auch nichts verfügbares.
Ich würde diesen gerne in meinem Pimatic Project einsetzen, kann aber kein Node.js etc. Eine Erweiterung ihrer Library würde mir sehr weiterhelfen.
Vielen Dank
Sebastian
12. Mrz. 2020 um 9:55
Soweit ich das sehe ist die Datenübertragung zu dieser Ausgangskarte eine andere, als beim PCF8574. Lediglich die Verwendung des I²C-Bus ist gleich.
Es müsste also ein extra Node.js Modul (und ein Pimatic Plugin) für die Karte erstellt werden. Die Befehle für die Karte sehen aber recht simpel aus, sodass das keine große Sache sein sollte.
Da ich die Hardware nicht habe, kann ich hier leider kaum helfen.
12. Mrz. 2020 um 12:07
Vielen Dank für die schnelle Antwort.
Dann wird es wohl zeit Node.js zu lernen.
11. Nov. 2020 um 16:57
Does this support npm version 6.14? It looks like it does not support more recent versions?
11. Nov. 2020 um 20:11
The npm version doesn’t matter, the node version matters.
Currently this node module uses an outdated version of the underlying i2c-bus module which doesn’t support node v14.
I’ll have a look at it and release an update as soon as possible.
12. Nov. 2020 um 14:40
Update done. The new v2.0.1 supports Node.js 10, 12 and 14.