Protokoll zwischen TXT4 und einem selbstgebauten „I2C-Target“

Alles rund um TX(T) und RoboPro, mit ft-Hard- und Software
Computing using original ft hard- and software
Forumsregeln
Bitte beachte die Forumsregeln!
Antworten
Arnoud-Whizzbizz
Beiträge: 204
Registriert: 20 Mär 2021, 17:06
Kontaktdaten:

Protokoll zwischen TXT4 und einem selbstgebauten „I2C-Target“

Beitrag von Arnoud-Whizzbizz » 02 Apr 2026, 13:22

Ich arbeite gerade an der Entwicklung einer neuen Hardware mit einem ATMega328-Prozessor. Seit einigen Wochen besitze ich einen TXT4-Controller, und nun habe ich mir vorgenommen, mein (zukünftiges) Gerät auch über I2C vom TXT4 aus steuerbar zu machen. Ich musste sofort an juhs I2CWrapper-Klasse denken, die ich mir schon vor einigen Jahren einmal angesehen hatte, als ich meinen Sketch „CNCv4_Board_3_Steppers.ino“ für AccelStepperI2C erstellte (und den joh sogar in die Beispiele seiner I2CWrapper-Bibliothek aufnahm). Mein Beispiel hatte damals jedoch noch wenig mit I2C-Kommunikation zu tun. :lol:

Jetzt versuche ich (am liebsten auf möglichst einfache Weise), zunächst einmal die I2C-Kommunikation zwischen dem TXT4 und einem Arduino UNO herzustellen. Für den Test habe ich die I2C-level-shifters, einen Druckknopf und einen Potentiometer auf einer kleinen Experimentierplatine aufgebaut. Die Idee ist nun, in ROBO Pro Coding eine I2C Anfrage zu stellen und z. B. den Potentiometer auszulesen und damit einen Servo anzusteuern.

IMG_2136.jpeg
IMG_2136.jpeg (1.49 MiB) 158 mal betrachtet

Ich brauche die CRC8-Prüfung und das Unit-Byte in der Anfrage der ursprünglichen I2Cwrapper-Firmware.ino zunächst nicht und versuche, das Hinzufügen von „eigener Hardware“ zur bestehenden I2Cwrapper-Bibliothek (was offenbar über die Precompiler-Direktive „MF_STAGE“ geregelt wird) noch etwas aufzuschieben. Außerdem hatte ich gehofft, dass ich die gesamte State-Machine-Architektur von I2Cwrapper nicht benötigen würde. Deshalb habe ich die (alte) AccelStepperI2C-„Slave“-Firmware auf das Wesentliche reduziert und auf den UNO geladen. Für meine Puffer habe ich eine abgespeckte Version von SimpleBuffer (ohne CRC-Prüfung und Unit-Byte) verwendet.

Bisher bekomme ich meine Versuche jedoch nicht zuverlässig zum Laufen. Ich versuche nun herauszufinden, ob dies daran liegt, dass mein technischer Ansatz vielleicht zu naiv ist. Ich stoße nämlich auf ein vermutlich recht grundlegendes Timing-Problem.

Das Problem ist nun, dass in dem Moment, in dem die Werte (der Potentiometerwert) der Anfrage zurückgefordert werden, diese noch nicht verfügbar sind. Dadurch hinkt der bufferOut also immer eine Anfrage des TXT4 hinterher. Die angeforderten Werte erscheinen zu spät im bufferOut und bleiben dort bis zur nächsten Anfrage stehen. Dies liegt daran, dass das requestEvent des TXT4 bereits eintrifft, bevor in der Hauptschleife der Befehl interpretiert, der Potentiometer ausgelesen und der zurückzugebende Wert (als zwei Bytes) in den bufferOut geschrieben wurde.

Im Idealfall sollte der TXT4 diesen Rückgabewert ignorieren und den Wert erneut abfragen? Ich sehe, dass der I2C-Wrapper über eine Funktion „autoAdjustI2Cdelay“ verfügt und einen „tainted“-Status für den „bufferOut“ usw. hat. Meine konkrete Frage lautet nun: Ist das alles notwendig, oder wurde bereits getestet, ein eigenes „Target“ (vorzugsweise ein Arduino UNO) über I2C vom TXT4 aus abzufragen? Oder wie lässt sich der bestehende I2C-Wrapper hierfür möglichst übersichtlich nutzen?

Vermutlich muss ich dann eine eigene XXX_firmware.h-Datei mit der Befehlsinterpretation dafür erstellen? Ich verstehe jedoch die Funktionsweise und Interpretation der „MF_STAGE“-Precompiler-Direktive hierfür noch nicht gut genug. Außerdem erscheint mir die gesamte Klasse für meine Anwendung überdimensioniert? Ich versuche schließlich, einen Endpunkt/Slave/Target zu erstellen, der ausschließlich vom TXT4 abgefragt werden kann. An diesem „Target“ sind keine anderen Sensoren usw. angeschlossen, die über I2C abgefragt oder angesteuert werden müssen.

Hat jemand (Jan selbst?) eine Idee, wie ich den „Handshake“ vom TXT4 auf I2C-Ebene hinbekomme, wenn die zurückzusendenden Daten nicht sofort (oder offensichtlich zu spät) verfügbar sind? :shock:

Benutzeravatar
fishfriend
Beiträge: 2457
Registriert: 26 Nov 2010, 11:45

Re: Protokoll zwischen TXT4 und einem selbstgebauten „I2C-Target“

Beitrag von fishfriend » 02 Apr 2026, 14:42

Hallo...
Auf die Schnelle würde ich einfache einen Wert z.B. 0 oder 1 zurücksenden und den dann im TXT 4.0 verwerfen, also vom TXT 4.0 eine neue Anfrage stellen, bis halt ein abweichender Wert kommt, der dann richtig ist.

Ich gebe zu, ich würde es ganz anders machen.
Warum nimmst du nicht das "einfache" Slavebeispiel der Arduino IDE?

Man könnte auch wie beim Onlinebetrieb vom TXT/TX/RoboIF... ein Datenfeld übertragen. Vorzugsweise wenn möglich den gleichen Aufbau.
Ich hab mich darum noch gekümmert, aber vermutlich wird der TXT 4.0 auch sowas haben.

Ist schon lustig, ich hab gerade einen ESP32 dem ich I2C am TXT beibringe :-)
Mit freundlichen Grüßen
Holger
ft Riesenräder PDF: ftcommunity.de/knowhow/bauanleitungen
TX-Light: Arduino und ftduino mit RoboPro

Arnoud-Whizzbizz
Beiträge: 204
Registriert: 20 Mär 2021, 17:06
Kontaktdaten:

Re: Protokoll zwischen TXT4 und einem selbstgebauten „I2C-Target“

Beitrag von Arnoud-Whizzbizz » 02 Apr 2026, 15:18

Danke für deine Antwort, Holger!
fishfriend hat geschrieben:
02 Apr 2026, 14:42
Auf die Schnelle würde ich einfache einen Wert z.B. 0 oder 1 zurücksenden und den dann im TXT 4.0 verwerfen, also vom TXT 4.0 eine neue Anfrage stellen, bis halt ein abweichender Wert kommt, der dann richtig ist.
Auf der sendenden Seite (UNO) stimmen die Daten. Sie sind lediglich zu spät verfügbar. Auf der empfangenden Seite (TXT4) lässt sich dies nicht erkennen. Es handelt sich schließlich um gültige Werte, nur aus der vorherigen Anfrage. :?
fishfriend hat geschrieben:
02 Apr 2026, 14:42
Warum nimmst du nicht das "einfache" Slavebeispiel der Arduino IDE?
Könntest du mir bitte innerhalb der Standardbeispiele der Arduino IDE den Weg zu einem allgemeinen, aber voll funktionsfähigen Beispiel dafür weisen? Ich habe zwar schon einige davon studiert, und die Technik ist ähnlich. Es werden zwei Interrupt-Handler (onReceive und onRequest) eingerichtet, die das eigentliche Empfangen und Senden der Bytes übernehmen sollen.

Meine Frage ist vielleicht eher eine Frage des Verständnisses: Woher nimmt der UNO die Zeit, um die angeforderte Antwort zu finden? Erst danach kann sie als Antwort gesendet werden. Aber worauf ich jetzt stoße, ist, dass die Anfrage bereits fertig ist und der TXT4 seine Antwort bereits angefordert hat.

Soweit ich das überblicken kann, sieht das I2C-Protokoll keinen echten „Handshake“ vor, durch den dem Master mitgeteilt werden könnte, dass die Antwort bald kommt. Es gibt zwar so etwas wie „Clockcycle-Stretching“, aber das scheint mir nicht die richtige Methode zu sein.

Und selbst wenn es möglich wäre, den TXT4 zu zwingen, die Anfrage erneut zu stellen, würde dies meiner Meinung nach nichts lösen. Schließlich müsste dann wieder dieselbe Verzögerungszeit eintreten...
fishfriend hat geschrieben:
02 Apr 2026, 14:42
Man könnte auch wie beim Onlinebetrieb vom TXT/TX/RoboIF... ein Datenfeld übertragen. Vorzugsweise wenn möglich den gleichen Aufbau.
Ich hab mich darum noch gekümmert, aber vermutlich wird der TXT 4.0 auch sowas haben.
In meinem Fall handelt es sich um einfache Antworten mit 1 oder 2 Bytes. Selbstverständlich sende ich dazu selbst einen Befehlscode mit. Das I2C-Protokoll (Wire-Bibliothek) fügt hier im Header die Länge dieser Nachricht hinzu.

Die Kommunikation vom TXT4 zu meinem „Target“ ist jedoch nicht das Problem. Es geht um das Timing der Antworten, die ich zurücksende.
fishfriend hat geschrieben:
02 Apr 2026, 14:42
Ist schon lustig, ich hab gerade einen ESP32 dem ich I2C am TXT beibringe :-)
Das ist interessant! Kann man über I2C Werte vom ESP32 abrufen?

Ich hoffe immer noch, dass ich etwas Einfaches übersehen habe. Ich werde noch einmal nach grundlegenden Beispielen suchen, insbesondere nach einer Anfrage, die auf Basis eines Befehlscodes auf dem UNO bestimmte Werte an den Controller zurückgeben kann.

Bisher sind jedoch alle Beispiele, die ich gefunden habe, Beispiele für die Kommunikation von und zu Hardware (wie Sensoren und Displays usw.), nicht für von Software emulierte Hardware.

Benutzeravatar
fishfriend
Beiträge: 2457
Registriert: 26 Nov 2010, 11:45

Re: Protokoll zwischen TXT4 und einem selbstgebauten „I2C-Target“

Beitrag von fishfriend » 02 Apr 2026, 16:47

Hallo...
Na ja, es ist schon etwas länger her mit meinen Experimenten zu I2C und dem UNO.
Auf der sendenden Seite (UNO) stimmen die Daten. Sie sind lediglich zu spät verfügbar. Auf der empfangenden Seite (TXT4) lässt sich dies nicht erkennen. Es handelt sich schließlich um gültige Werte, nur aus der vorherigen Anfrage. :?
Ich bin mir nicht sicher. Kann man den Buffer nach dem Senden löschen?
Man könnte dem Wert auch eine fortlaufende Nummer anhängen. Ob nun eine 0 und/oder der laufende Wert, der TXT 4.0 kann daran erkennen, das der Wert gültig ist oder nicht. Man kann ja auch nur bis 255 zählen und dann wieder von 0 anfangen.

Ich meine, der UNO macht das in der Hardware. Deswegen kann man ja auch nur bestimmte Pins nehmen. Wenn man Pech hat, sind nicht alle Möglichkeiten die I2C hat in der Lib zum UNO drinn.
Es gab/gibt I2C Beispiele, wo man zwei UNOs verbindet. Wenn das läuft, kann man nun den TXT 4.0 drannhängen. So dachte ich mir das. Zumindest hatte ich das damals so gemacht.

Das es da eine Art Timeout im TXT 4.0 gibt, glaub ich jetzt nicht. Im Grunde hat der Slave doch alle Zeit der Welt. Ich vermute mal das es da noch ein Flag gibt, wo man das Senden freigibt. Ich meine nicht, dass es emuliert ist. Zumindest beim UNO.

---
Mein Ansatz bei meinem momentanen Projekt ist noch ganz anders.
Erst wollte ich I2C am ESP direkt machen. Hab ich momentan verworfen.
Ich hab aber eine fertige Platine mit Display, die auch noch eine zusätzliche serielle Schnittstelle hat.
Es gibt I2C zu Seriell (z.B. von NXP). Der hat zwei serielle Schnittstellen. Ich wollte über den TXT 4.0 dann diesen Wandler ansprechen, der dann die Platine mit dem ESP32 anspricht, Der große Vorteil davon ist, dass man nichts bauen oder umbauen muss. Man kann es direkt anklemmen und dann das Programm aufspiele. Soll halt "nachbausicher" sein. So der Plan.

Mit freundlichen Grüßen
Holger
ft Riesenräder PDF: ftcommunity.de/knowhow/bauanleitungen
TX-Light: Arduino und ftduino mit RoboPro

juh
Beiträge: 1134
Registriert: 23 Jan 2012, 13:48

Re: Protokoll zwischen TXT4 und einem selbstgebauten „I2C-Target“

Beitrag von juh » 02 Apr 2026, 19:02

Hallo Arnoud,

das Hinzufügen eigener Module in I2Cwrapper ist gut dokumentiert. MF_STAGE spielt damit nur firmware-intern eine Rolle und sollte nicht angefasst werden müssen.

https://github.com/ftjuh/I2Cwrapper/tre ... wn-modules

Für dienen derzeitigen Aufbau bräuchte es gar kein neues Modul, das PinI2C-Modul kann Pins analog und digital lesen und schreiben. Es gibt also keine Notwendigkeit den Code auf Target-Seite zu ändern. Einfach in firmware_modules.h PinI2C_firmware.h auskommentieren und hochladen.

I2Cwrapper ist aber für dich aus zwei Gründen nicht geeignet bzw. überdimensioniert.

Erstens ist der Hauptzweck, das I2C-Interface so zu abstrahieren, dass man Hardware, die am Target (früher: Slave) hängt, (fast) genauso verwenden kann, als wenn sie lokal angeschlossen wäre. Ich steuere den Servo, den Schrittmotor etc. über I2C fast mit dem gleichen Code, mit dem ich sie direkt ansteuern würde, indem I2C-Versionen der entsprechenden libraries das identische Interface bereit stellen, aber alles über I2C kapseln.

Zweitens geht I2Cwrapper davon aus, dass der Controller/Master ein Arduino-artiger ist. Die I2C-Versionen der libraries zum ansprechen des I2C-Target müssten also für einen TXT neu geschrieben werden. Du müsstest also auf TXT4-Seite für PinI2C diese Funktionen (oder nur die, die du brauchst) nachprogrammieren:

https://github.com/ftjuh/I2Cwrapper/blo ... PinI2C.cpp

Da sieht man aber schon das Problem, denn auch die wrapper-Klasse müsste man portieren. Und es wäre etwas witzlos, denn der Sinn des ganzen ist ja gerade, gewohnte Arduino-libraries transparent über I2C nutzen zu können.

Zur Thematik insgesamt ist zu sagen, dass per µC "emulierte" I2C-Targets immer das Timing-Problem haben. Wegen der strikt unidirektionalen Kommunikation kann das Target nie Bescheid sagen: "Ich bin fertig und bereit für die nächste Aufgabe" oder "Ich habe neue Daten", es sei denn man nutzt eine extra Interrupt-Verbindung. I2C-Wrapper hat genau dafür verschiedene Mechanismen wie die State machine, Checksums, I2Cdelay, oder eine optionale Interrupt-Leitung, die das System robuster machen sollen, das Grundproblem bleibt aber.

Wenn der Controller nämlich den nächsten Befehl schickt (oder das nächste Register liest/schreibt, um in der klassischen Terminologie zu bleiben), obwohl das Target das vorige noch nicht verarbeitet hat, kommt es zu Problemen. Bei echter I2C Hardware gibt es dafür clock stretching, ATmegas können das auch, die Implementierung in der Wire-Library war aber immer ein Thema.

Man sollte auf Controller-seite also in jedem Fall immer ein paar Takte warten, bei I2Cwrapper ist der default 20ms äußerst üppig bemessen, nur bei zeitintensiveren Sachen wie OLED-Ansteuerung kann mehr nötig sein.

Was genau in deinem Beispiel das Problem ist, kann ich nicht sagen, weil ich nicht nachvollziehen kann, wie dein Code aussieht, und ich mich mit den TXTs nicht auskenne. Auf jeden Fall sollten da aus den o.g. Gründen Pausen in den Controller-Code zwischen zwei Abfragen.

Ich würde aber meinen, dass es ausreichend Beispiele geben sollte, die ähnliches schon gemacht haben. Von Dirk gibt es ja eine ganze Serie zu I2C in der ftPedia, da sollte die Controller-Seite gut dargestellt sein. Und auf UNO-Seite sollte es einer der üblichen Slave-Sketche tun, da hat Holger ja schon was geschrieben, wenn ich das richtig verstehe (leider verstehe ich Holger meistens nicht).

Prinzipiell kannst du jeden µC (nicht unbedingt: Kleincomputer wie Rasperry mit OS) per I2C ansprechen, notfalls per Software emuliert, wenn er schnell genug ist, auch den ESP32. Der war allerdings lange nur eingeschränkt brauchbar als I2C-Target, erst seit einer Weile soll das gelöst sein, allerdings nach wie vor ohne clock stretching, selbst probiert habe ich es noch nicht.

vg
Jan

Arnoud-Whizzbizz
Beiträge: 204
Registriert: 20 Mär 2021, 17:06
Kontaktdaten:

Re: Protokoll zwischen TXT4 und einem selbstgebauten „I2C-Target“

Beitrag von Arnoud-Whizzbizz » 02 Apr 2026, 19:29

fishfriend hat geschrieben:
02 Apr 2026, 16:47
Es gibt I2C zu Seriell (z.B. von NXP). Der hat zwei serielle Schnittstellen. Ich wollte über den TXT 4.0 dann diesen Wandler ansprechen, der dann die Platine mit dem ESP32 anspricht, Der große Vorteil davon ist, dass man nichts bauen oder umbauen muss.
Ja, das Löschen des Ausgangspuffers ist möglich, aber der Puffer enthält zu diesem Zeitpunkt eben genau die angeforderten und noch nie übertragenen Daten. Die I2C-Kommunikation mit dem UNO ist nicht das Problem, I2C ist in der Wire/TwoWire-Klasse vollständig definiert. Und ich habe das Protokoll auch auf Schnittstellenebene untersucht. Auch meine level-umsetzer funktionieren. Das Problem liegt also meiner Meinung nach in meine Software. Es ist nicht so, dass ich überhaupt keine Daten von meinem UNO zum TXT4 bekomme, nein, das funktioniert, nur ist mir noch nicht klar, wie ich die Bearbeitung des onRequest-Ereignisses so verzögern kann, dass die Daten tatsächlich im Puffer verfügbar sind. :roll:

Ein Metasystem mit einem eigenen Zähler ist eine Idee, aber ich habe einfach das Gefühl, dass ich etwas anderes übersehe. Das Beispiel mit den zwei Arduinos habe ich auch gesehen, aber ich hatte gehofft, die Kommunikation auf Anhieb hinzubekommen. Ich muss noch ein bisschen weiter daran basteln.... :lol:

Ich sehe auch eine ausführliche Antwort von Jan. Vielleicht sollte ich etwas genauer erläutern, warum ich unbedingt über I2C (oder, falls das nicht klappt, über den CAN-Bus?) zwischen dem TXT4 und meiner zukünftigen Hardware kommunizieren möchte. Das liegt nicht daran, dass ich so ein großer TXT4-Fan bin (ich habe mir nach langem Hin und Her erst vor einigen Wochen einen solchen Controller zugelegt), sondern einzig und allein daran, dass ich anderen in Zukunft eine einfache Möglichkeit bieten möchte, diese Hardware anzuschließen und zu nutzen. Vor einigen Wochen hatte ich noch nicht einmal mit ROBO Pro Coding gearbeitet, daher liegt die Herausforderung zum Teil auch darin. Mit I2C habe ich (zwischen ESP32 oder Arduinos und verschiedenen Sensoren und Geräten) schon sehr viel gemacht. Die Herausforderung besteht diesmal wirklich darin, eine zuverlässig funktionierende I2C-Verbindung zwischen einem Fischertechnik-Controller und meiner eigenen Bastelei herzustellen.

Benutzeravatar
fishfriend
Beiträge: 2457
Registriert: 26 Nov 2010, 11:45

Re: Protokoll zwischen TXT4 und einem selbstgebauten „I2C-Target“

Beitrag von fishfriend » 02 Apr 2026, 19:32

Hallo...
Nachtrag
Es gibt da Beispiele bei:
Ardinohttps://docs.arduino.cc/learn/communication/wire/
oder bei
https://hlembke.de/arduinoablage/crate.php?20150411i2c
...
Mit freundlichen Grüßen
Holger
ft Riesenräder PDF: ftcommunity.de/knowhow/bauanleitungen
TX-Light: Arduino und ftduino mit RoboPro

juh
Beiträge: 1134
Registriert: 23 Jan 2012, 13:48

Re: Protokoll zwischen TXT4 und einem selbstgebauten „I2C-Target“

Beitrag von juh » 02 Apr 2026, 19:33

PS1: Die Standard-Antwort ist übrigens: show your code. ;-)

PS2: ich habe spaßeshalber mal Claude Code gebeten, die Portierung zu machen, meine Erfahrungen mit anderen Projekten sind extrem gut, ist aber natürlich ungetestet. Wenn du also die Standard-Firmware mit aktiviertem PinI2C Modul auf den UNO lädst und die Portierung stimmt, sollte das so gehen. Aber man sieht das Problem: lohnend wäre das nur, wenn du eine der schon implementierten komplexeren Module wie AccelStepper nutzen willst. Um ein paar Pins abzufragen, ist das massiver Overkill. Ansonsten also besser mit so etwas starten statt I2Cwrapper zu modifizieren.

https://projecthub.arduino.cc/PIYUSH_K_ ... ion-31a095

Code: Alles auswählen

"""
I2Cwrapper PinI2C module for fischertechnik TXT 4.0 controller.

This is a Python port of the controller-side I2Cwrapper and PinI2C classes
from https://github.com/ftjuh/I2Cwrapper for use on the TXT 4.0 via smbus2.

The Arduino target must run the I2Cwrapper firmware with PinI2C module enabled.

Usage example:
    from i2cwrapper_pin import I2CwrapperPin

    pin = I2CwrapperPin(0x08)  # default I2C address
    pin.reset()
    pin.pin_mode(13, pin.OUTPUT)
    pin.digital_write(13, 1)
    val = pin.digital_read(2)
    analog = pin.analog_read(0)  # A0
    pin.analog_write(9, 128)
    pin.close()

TXT 4.0 uses I2C bus 3 (EXT1/EXT2 connectors), 3.3V logic.
Make sure to use level shifters if the target runs at 5V.

License: GPL-2.0 (same as I2Cwrapper)
"""

import time
import struct
from smbus2 import SMBus, i2c_msg


# ---------------------------------------------------------------------------
# CRC8 (same algorithm as I2Cwrapper's SimpleBuffer CRC8)
# Polynomial: x^8 + x^5 + x^4 + 1  => 0x8C (reflected) / 0x31 (normal)
# I2Cwrapper uses the Dallas/Maxim 1-Wire CRC (polynomial 0x8C, init 0)
# ---------------------------------------------------------------------------

def _crc8(data: bytes) -> int:
    """Compute CRC8 as used by I2Cwrapper's SimpleBuffer (Dallas/Maxim 1-Wire CRC)."""
    crc = 0
    for byte in data:
        crc ^= byte
        for _ in range(8):
            if crc & 1:
                crc = (crc >> 1) ^ 0x8C
            else:
                crc >>= 1
    return crc & 0xFF


# ---------------------------------------------------------------------------
# Constants matching I2Cwrapper.h
# ---------------------------------------------------------------------------

I2C_DEFAULT_ADDRESS = 0x08
I2C_MAX_BUF = 20
I2C_DEFAULT_DELAY = 0.020  # 20 ms, same as Arduino default

# Core wrapper commands (240-255 range)
RESET_CMD = 241
CHANGE_I2C_ADDRESS_CMD = 242
SET_INTERRUPT_PIN_CMD = 243
CLEAR_INTERRUPT_CMD = 244
CLEAR_INTERRUPT_RESULT = 1  # 1 byte
GET_VERSION_CMD = 245
GET_VERSION_RESULT = 4  # 4 bytes (uint32)
PING_BACK_CMD = 246

# PinI2C commands (060-069 range)
PIN_CMD_OFFSET = 60
PIN_PINMODE_CMD = PIN_CMD_OFFSET + 0
PIN_DIGITAL_READ_CMD = PIN_CMD_OFFSET + 1
PIN_DIGITAL_READ_RESULT = 2  # int16
PIN_DIGITAL_WRITE_CMD = PIN_CMD_OFFSET + 2
PIN_ANALOG_READ_CMD = PIN_CMD_OFFSET + 3
PIN_ANALOG_READ_RESULT = 2  # int16
PIN_ANALOG_WRITE_CMD = PIN_CMD_OFFSET + 4
PIN_ANALOG_REFERENCE_CMD = PIN_CMD_OFFSET + 5

# Fake unit number for PinI2C (same as Arduino side)
PIN_UNIT = 253


# ---------------------------------------------------------------------------
# I2CwrapperPin  -  combined wrapper + PinI2C in one flat class
# ---------------------------------------------------------------------------

class I2CwrapperPin:
    """Combined I2Cwrapper + PinI2C controller for the TXT 4.0.

    Talks to an Arduino target running I2Cwrapper firmware with PinI2C enabled.
    """

    # Arduino pin mode constants (match Arduino.h)
    INPUT = 0x0
    OUTPUT = 0x1
    INPUT_PULLUP = 0x2

    LOW = 0
    HIGH = 1

    def __init__(self, address: int = I2C_DEFAULT_ADDRESS, bus: int = 3,
                 i2c_delay: float = I2C_DEFAULT_DELAY):
        """
        Args:
            address: 7-bit I2C address of the target (default 0x08).
            bus: Linux I2C bus number (TXT 4.0 = 3).
            i2c_delay: Minimum pause between I2C transactions in seconds.
        """
        self.address = address
        self.bus_num = bus
        self.i2c_delay = i2c_delay
        self._last_tx = 0.0
        self.sent_ok = False
        self.result_ok = False
        self._sent_errors = 0
        self._result_errors = 0

    # ------------------------------------------------------------------
    # Low-level I2C helpers (replicate SimpleBuffer + Wire semantics)
    # ------------------------------------------------------------------

    def _do_delay(self):
        """Wait until i2c_delay has passed since the last transmission."""
        elapsed = time.time() - self._last_tx
        remaining = self.i2c_delay - elapsed
        if 0 < remaining <= self.i2c_delay:
            time.sleep(remaining)

    def _build_message(self, cmd: int, unit: int, params: bytes = b"") -> bytes:
        """Build a complete I2Cwrapper message with CRC8 header.

        Wire format:  [CRC8] [cmd] [unit] [param0] [param1] ...
        CRC8 is computed over bytes [1..n], then placed at [0].
        """
        payload = bytes([cmd, unit]) + params
        crc = _crc8(payload)
        return bytes([crc]) + payload

    def _send_command(self, cmd: int, unit: int, params: bytes = b"") -> bool:
        """Send a command to the target. Returns True on success."""
        self._do_delay()
        msg_bytes = self._build_message(cmd, unit, params)
        try:
            msg = i2c_msg.write(self.address, msg_bytes)
            with SMBus(self.bus_num) as bus:
                bus.i2c_rdwr(msg)
            self.sent_ok = True
        except OSError:
            self.sent_ok = False
            self._sent_errors += 1
        self._last_tx = time.time()
        return self.sent_ok

    def _read_result(self, num_bytes: int) -> bytes | None:
        """Request num_bytes (+ 1 CRC8 byte) from the target.

        Returns the payload bytes (without CRC8) on success, or None on failure.
        """
        self._do_delay()
        total = num_bytes + 1  # +1 for CRC8
        try:
            msg = i2c_msg.read(self.address, total)
            with SMBus(self.bus_num) as bus:
                bus.i2c_rdwr(msg)
            raw = bytes(msg)
            self._last_tx = time.time()
        except OSError:
            self.result_ok = False
            self._result_errors += 1
            self._last_tx = time.time()
            return None

        # raw[0] = CRC8, raw[1:] = payload
        received_crc = raw[0]
        payload = raw[1:]
        expected_crc = _crc8(payload)
        if received_crc == expected_crc:
            self.result_ok = True
            return payload
        else:
            self.result_ok = False
            self._result_errors += 1
            return None

    def _send_and_receive(self, cmd: int, unit: int, params: bytes,
                          result_len: int) -> bytes | None:
        """Send command, then read result. Returns payload or None."""
        if not self._send_command(cmd, unit, params):
            return None
        return self._read_result(result_len)

    # ------------------------------------------------------------------
    # Wrapper core functions
    # ------------------------------------------------------------------

    def ping(self) -> bool:
        """Test if target is responding on the I2C bus."""
        try:
            with SMBus(self.bus_num) as bus:
                msg = i2c_msg.write(self.address, [])
                bus.i2c_rdwr(msg)
            return True
        except OSError:
            return False

    def reset(self, reset_delay: float = 0.1):
        """Reset the target firmware to its initial state.

        Args:
            reset_delay: Seconds to wait after reset for target to re-init.
        """
        self._send_command(RESET_CMD, 0xFF)
        time.sleep(reset_delay)

    def get_version(self) -> int | None:
        """Get target firmware version as uint32 (major.minor.patch packed).

        Returns None on error, otherwise an int where:
            bits 0-7   = major
            bits 8-15  = minor
            bits 16-23 = patch
        """
        result = self._send_and_receive(GET_VERSION_CMD, 0xFF, b"",
                                        GET_VERSION_RESULT)
        if result and len(result) >= 4:
            return struct.unpack("<I", result[:4])[0]
        return None

    def get_version_string(self) -> str | None:
        """Get firmware version as human-readable string, e.g. '0.6.1'."""
        v = self.get_version()
        if v is None:
            return None
        major = v & 0xFF
        minor = (v >> 8) & 0xFF
        patch = (v >> 16) & 0xFF
        return f"{major}.{minor}.{patch}"

    def set_i2c_delay(self, delay_sec: float) -> float:
        """Set minimum delay between I2C transactions (in seconds).

        Returns the previously set delay.
        """
        old = self.i2c_delay
        self.i2c_delay = delay_sec
        return old

    def change_i2c_address(self, new_address: int):
        """Permanently change the target's I2C address (needs addressFromFlash module)."""
        self._send_command(CHANGE_I2C_ADDRESS_CMD, 0xFF,
                           bytes([new_address]))

    def set_interrupt_pin(self, pin: int, active_high: bool = True):
        """Configure the target's interrupt output pin."""
        self._send_command(SET_INTERRUPT_PIN_CMD, 0xFF,
                           bytes([pin & 0xFF, 1 if active_high else 0]))

    def clear_interrupt(self) -> int:
        """Acknowledge interrupt and get reason. Returns 0xFF on error."""
        result = self._send_and_receive(CLEAR_INTERRUPT_CMD, 0xFF, b"",
                                        CLEAR_INTERRUPT_RESULT)
        if result and len(result) >= 1:
            return result[0]
        return 0xFF

    def sent_errors(self) -> int:
        """Return and reset count of failed send operations."""
        e = self._sent_errors
        self._sent_errors = 0
        return e

    def result_errors(self) -> int:
        """Return and reset count of failed receive operations."""
        e = self._result_errors
        self._result_errors = 0
        return e

    def transmission_errors(self) -> int:
        """Return and reset total count of all I2C errors."""
        return self.sent_errors() + self.result_errors()

    # ------------------------------------------------------------------
    # PinI2C functions
    # ------------------------------------------------------------------

    def pin_mode(self, pin: int, mode: int):
        """Set pin mode (INPUT=0, OUTPUT=1, INPUT_PULLUP=2)."""
        self._send_command(PIN_PINMODE_CMD, PIN_UNIT,
                           bytes([pin & 0xFF, mode & 0xFF]))

    def digital_read(self, pin: int) -> int:
        """Read digital pin state. Returns 0, 1, or -1 on error."""
        result = self._send_and_receive(
            PIN_DIGITAL_READ_CMD, PIN_UNIT,
            bytes([pin & 0xFF]),
            PIN_DIGITAL_READ_RESULT)
        if result and len(result) >= 2:
            return struct.unpack("<h", result[:2])[0]
        return -1

    def digital_write(self, pin: int, value: int):
        """Write digital pin (0=LOW, 1=HIGH)."""
        self._send_command(PIN_DIGITAL_WRITE_CMD, PIN_UNIT,
                           bytes([pin & 0xFF, value & 0xFF]))

    def analog_read(self, pin: int) -> int:
        """Read analog pin (10-bit ADC, 0-1023). Returns -1 on error."""
        result = self._send_and_receive(
            PIN_ANALOG_READ_CMD, PIN_UNIT,
            bytes([pin & 0xFF]),
            PIN_ANALOG_READ_RESULT)
        if result and len(result) >= 2:
            return struct.unpack("<h", result[:2])[0]
        return -1

    def analog_reference(self, mode: int):
        """Set analog reference mode (AVR only, ESPs don't support this)."""
        self._send_command(PIN_ANALOG_REFERENCE_CMD, PIN_UNIT,
                           bytes([mode & 0xFF]))

    def analog_write(self, pin: int, value: int):
        """Write PWM value (0-255) to pin."""
        self._send_command(PIN_ANALOG_WRITE_CMD, PIN_UNIT,
                           bytes([pin & 0xFF]) + struct.pack("<h", value))

    def close(self):
        """No-op for API symmetry (smbus2 opens/closes per transaction)."""
        pass


# ---------------------------------------------------------------------------
# Quick test / demo
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    print("I2Cwrapper PinI2C - TXT 4.0 test")
    target = I2CwrapperPin(0x08)

    if not target.ping():
        print("Target not found at address 0x{:02X}".format(target.address))
    else:
        print("Target found.")
        target.reset()
        time.sleep(0.1)

        ver = target.get_version_string()
        print(f"Firmware version: {ver}")

        # Example: blink LED on pin 13
        target.pin_mode(13, target.OUTPUT)
        for i in range(6):
            target.digital_write(13, i % 2)
            time.sleep(0.5)

        # Example: read analog
        val = target.analog_read(0)
        print(f"Analog A0 = {val}")

        errs = target.transmission_errors()
        print(f"Transmission errors: {errs}")

    print("Done.")

Arnoud-Whizzbizz
Beiträge: 204
Registriert: 20 Mär 2021, 17:06
Kontaktdaten:

Re: Protokoll zwischen TXT4 und einem selbstgebauten „I2C-Target“

Beitrag von Arnoud-Whizzbizz » 02 Apr 2026, 20:00

Danke, Jan, für deine ausführliche Antwort. Ich habe mir die Dokumentation bisher durchgesehen und zunächst selbst versucht, das Protokoll auf Low-Level-Ebene hinzubekommen. Dabei habe ich mir deine Bibliotheken, den neuen I2C-Wrapper, aber auch die Vorgänger, genau angesehen. Aber wenn ich z. B. den CRC8-Code auch in meinen Nachrichten akzeptiere, muss ich diesen CRC8-Check wohl auch auf der ROBO Pro Coding-Seite einbauen. Das lässt sich sicher alles in Python umsetzen (und vielleicht ist es dort ja schon vorhanden), aber ich hatte gehofft, dass das für diesen schnellen Proof-of-Concept eigentlich nicht nötig ist.
juh hat geschrieben:
02 Apr 2026, 19:02
I2Cwrapper ist aber für dich aus zwei Gründen nicht geeignet bzw. überdimensioniert.
Ich stimme dir in der Tat zu, dass der I2C-Wrapper für meine relativ einfache Anwendung viel zu umfangreich und mit zu vielen Funktionen ausgestattet ist. Außerdem kann ich die PinI2C-Funktionen nicht direkt nutzen (da ich die „Daten“ nicht direkt von den Pins ablesen kann; es handelt sich hier um eine PS2-Fernbedienung, die ich auf dem TXT4 verfügbar machen möchte. Ich weiß, dass das auch schon über BT möglich ist, aber das ist etwas anderes...) Ich bin mir auch des eigentlichen Zwecks von I2Cwrapper bewusst, daher meine Idee, das „Target“-Verhalten aus der firmware.ino zu abstrahieren. Es ist auch sehr interessant, wie du die Zustandsmaschine und die Prüfsummen usw. hinzugefügt hast, obwohl mir klar ist, dass diese hauptsächlich für die Kommunikation zwischen Arduinos gedacht sind. Eine Möglichkeit wie Clock Stretching muss ich noch weiter vertiefen. Der nächste Schritt ist auf jeden Fall, das Ganze mal auf den Logikanalysator zu setzen, denn das Loggen auf Serial macht die Sache natürlich ohnehin nicht schneller.

Ich bin tatsächlich dabei, auf dem TXT4 (hauptsächlich in Python) die erforderlichen Funktionen zu schreiben. Im Grunde funktionieren diese bereits (für isolierte Anfragen), nur stoße ich noch auf dieses Kommunikationsproblem, für das ich auf eine einfache Lösung gehofft hatte, bevor ich sehen kann, wie die Reaktion und die mögliche Latenzzeit beispielsweise eines Servos aussehen und ob diese ganze Idee überhaupt jemals realisierbar ist. Aber für mich ist das Suchen und Herausfinden auch eine schöne Herausforderung. Ich habe inzwischen jedoch festgestellt, dass es sich um Pionierarbeit handelt... :lol:

Das Fehlen eines Hardware-„Handshakes“ bei I2C ist mir bekannt, aber wahrscheinlich nicht das Problem. Allerdings könnte ich an die maximale Taktrate des ATMega stoßen (die mit 16 MHz nicht besonders hoch ist), aber ich experimentiere noch ein wenig weiter.

Verschiedene ft:pedia-Artikel, eine I2C-Veröffentlichung von Axel Chobe und das sehr umfangreiche Buch „TXT4.0 Internals and Programming“ von David Adams habe ich ebenfalls schon durchgesehen, aber logischerweise geht es darin hauptsächlich um die Anbindung an bestehende I2C-Hardware.

Ich halte euch auf dem Laufenden, vielen Dank für all die bisherigen Beiträge! :P

Arnoud-Whizzbizz
Beiträge: 204
Registriert: 20 Mär 2021, 17:06
Kontaktdaten:

Re: Protokoll zwischen TXT4 und einem selbstgebauten „I2C-Target“

Beitrag von Arnoud-Whizzbizz » 02 Apr 2026, 20:13

juh hat geschrieben:
02 Apr 2026, 19:33
PS1: Die Standard-Antwort ist übrigens: show your code. ;-)
Ha, klar, aber in diesem Fall reicht Pseudocode aus, dachte ich. Denn ich verstehe ziemlich genau, warum es nicht funktioniert, und suche gerade nach Lösungen im Code anderer oder nach Erfahrungsberichten von jemandem, der erklären könnte, wie sich solche Anfragen in Software überhaupt zufriedenstellend lösen lassen, angesichts der Latenz, die man bei einer Softwarelösung immer haben wird. Es handelt sich nun mal nicht um einen Temperatursensor, der die angeforderten Werte einfach so in einem Register gespeichert hat und sie nur noch ausgeben muss. Für manche Funktionen muss meine Hardware erst einmal einiges sammeln. Und außerdem droht eine Überlastung des Busses, da der TXT4 alles abfragen muss, weil es nicht auf Interrupt-Basis möglich ist.
juh hat geschrieben:
02 Apr 2026, 19:33
PS2: ich habe spaßeshalber mal Claude Code gebeten, die Portierung zu machen, meine Erfahrungen mit anderen Projekten sind extrem gut, ist aber natürlich ungetestet.
Haha, Claude Code kommt ans Ende, denke ich! Ich glaube, der CRC8-Code könnte noch nützlich sein! Danke! Ich bin jedoch noch nicht im Bereich „Zuverlässigkeit“ angekommen und arbeite in Bezug auf die I2C-Kommunikation noch an den Grundlagen. ;)

juh
Beiträge: 1134
Registriert: 23 Jan 2012, 13:48

Re: Protokoll zwischen TXT4 und einem selbstgebauten „I2C-Target“

Beitrag von juh » 02 Apr 2026, 21:11

Na klar, Schuld ist immer der Code der anderen. ;-)
Arnoud-Whizzbizz hat geschrieben:
02 Apr 2026, 20:13
Für manche Funktionen muss meine Hardware erst einmal einiges sammeln. Und außerdem droht eine Überlastung des Busses, da der TXT4 alles abfragen muss, weil es nicht auf Interrupt-Basis möglich ist.
Vielleicht zu offensichtlich, aber wäre nicht das klassische Vorgehen, die Daten in der loop() zu pollen und beim Interrupt den letzten vollständigen Stand zu übermitteln? Variablen dann volatile natürlich.
vg
Jan

Antworten