Divoom Aurabox + Timebox + TimeboxEvo [Update, kommunizieren funktioniert]

Begonnen von schwatter, 25 Dezember 2017, 23:37:43

Vorheriges Thema - Nächstes Thema

schwatter

Komme im Moment nicht weiter. Auf dem ZeroW ist eigentlich alles richtig eingerichtet. Es kommt aber nichts als Antwort zurück.
Nicht mal z.B. AT+BRSF=63 auf Kanal3.

Bin jetzt wieder auf dem Raspi3 und teste. Was beide aber Gemeinsam haben, sende ich auf Kanal 1 bleibe ich in einer Schleife hängen.

root@raspiFhem:/opt/fhem/divoom# perl tbetest.pl
Create RFCOMM client (11:75:68:C9:7D:36)...


Kannst du mehr Debugging einbauen?

schwatter

Whoop, ich habe mal gewartet. Jetzt kam doch was....

Create RFCOMM client (11:75:68:C9:7D:36)...
Device answer: Up
done

schwatter

So, jetzt konnte ich nachstellen, wann ich das "Up" bekomme.

Und zwar hänge ich die ganze Zeit in der Schleife. Sobald Fhem
per TTS eine Audioausgabe sendet (Ich denke mal generell irgendwas per Audio),
endet die Schleife und es kommt die Ausgabe mit "UP".

Danach klappt das aber nicht mehr. Erst ein Restart hilft. Wahrscheinlich schießt das
RFCOMM ab.

edit:
Ja, nun kann ich es bestätigen. Audio kommt, Schleife endet und "Up" wird ausgegeben.

mumpitzstuff

Dann hängst du auf dem Port für die Audioausgabe. Das ist falsch. Für die Datenverbindung wird ein anderer Port benötigt. Bei deiner Aurabox kriegst du auch mit Port 1 oder 3 dieses AT irgendwas als Antwort. Für die Datenverbindung muss man aber Port 4 verwenden.
Ein Debugging kann ich nicht einbauen. An der Stelle rufe ich eine Bibliotheksfunktion auf und das wars. Ich habe keinerlei Einflussmöglichkeiten auf den Aufbau der Verbindung. Zur Not Versuchs doch mal mit dem Python Timebox Modul, vielleicht spuckt das mehr aus.

schwatter

#124
Mit Python komme ich auch nicht weiter. Dann muss ich wohl nochmal versuchen,
die App zu sniffen in Android.

edit:

Gerade gesehen. Vielleicht für dich noch interessant.

https://github.com/jbfuzier/timeboxmini

schwatter

#125
Ich habe mit Catlog einen Log erstellt. Dieser zeigt, das RFCOMM auf Channel 6 ist. mh......

edit:

Channel 6 hatte ich natürlich getestet. Vielleicht fällt dir noch was im Log auf. Bei
der Divoomapp hatte ich Daten und Cache gelöscht. Dann aufgerufen, kurz ein bisschen
hin und her gespielt und beendet.


mumpitzstuff

Channel 6 finde ich auch 2-3 mal. Daneben taucht aber ziemlich viel Audio Zeugs im Logfile auf. Ich bin ehrlich gesagt etwas ratlos und es beschleicht mich die Vermutung, das die Kommunikation mit der neuen Timebox völlig anders aufgebaut ist. So ein Scheiss...

Ich würde es noch mal mit dem bluetooth log versuchen, so wie ich es gesagt habe, damit man aurabox und timebox evo nebeneinander legen kann.

PS: das github gehupe kennst du bereits. Habe nur das Zeug auf den Server geschoben, was ich hier im Forum schon gepostet hatte.

schwatter

Soo,

nochmal Gedanken dazu und ich hab was gefunden  :) Und zwar nimm dir nochmal das File aus Beitrag 111.
Öffnen mit Wireshark, dann Filter auf SPP. Dann siehst du den Traffic vom handy zur Box und umgedreht.
Eine Zeile anklicken, dann "Bluetooth SPP Packet" ausklappen. Zack Rawmessages  ;D

Jetzt geh ich das mal durch, ob was funktioniert.

schwatter

Endlich, ich habe was umschalten können. Und zwar habe ich es geschafft, auf den manuellen Counter zu switchen.
Verwendet habe ich die timebox.py. Diese musste ich anpassen.

#!/usr/bin/env python

import os
import bluetooth
import math
from PIL import Image
from binascii import unhexlify
from itertools import product
from math import modf
from os import listdir
from os.path import isfile, join

import appdirs
import click
import click_spinner
import datetime
import dateutil.parser
from colour import Color

APPNAME = 'timebox'
VENDOR = 'scratch'

# configuration directory set using "appdirs"
CONFDIR = appdirs.user_data_dir('timebox', 'scr4tch')

# configuration file
CONFFILE = os.path.join(CONFDIR, 'known_devices')

# list of known devices (found by discovery, saved by user)
KNOWN_DEVICES = ["11:75:68:C9:7D:36"]

# initial connection reply
TIMEBOX_HELLO = [0, 5, 72, 69, 76, 76, 79, 0]
FILTERS = {
            'bicubic': Image.BICUBIC,
            'cubic': Image.CUBIC,
            'linear': Image.LINEAR,
            'bilinear': Image.BILINEAR,
            'normal': Image.NORMAL,
            'box': Image.BOX,
            'nearest': Image.NEAREST,
            'none': Image.NONE,
            'hamming': Image.HAMMING,
            'lanczos': Image.LANCZOS,
            'antialias': Image.ANTIALIAS
}


class Timebox:
    debug = False

    def __init__(self, target, debug=False):
        self.debug = debug

        if(isinstance(target, bluetooth.BluetoothSocket)):
            self.sock = target
            self.addr, _ = self.sock.getpeername()
        else:
            self.sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
            self.addr = target
            self.sock.connect((self.addr, 1))

    def connect(self):
        if(not self.sock):
            self.sock.connect((self.addr, 1))
            ret = self.sock.recv(256)
            if(self.debug):
                click.echo("-> %s" % [ord(c) for c in self.sock.recv(256)])

    def disconnect(self):
        self.sock.close()

    def send(self, package, recv=True):
        if (self.debug):
            click.echo("-> %s" % [hex(b)[2:].zfill(2) for b in package])
        self.sock.send(str(bytearray(package)))

        if(recv):
            ret = [ord(c) for c in self.sock.recv(256)]

            if(self.debug):
                click.echo("<- %s" % [hex(h)[2:].zfill(2) for h in ret])

    def send_raw(self, bts):
        self.sock.send(bts)


VIEWTYPES = {
    "clock": 0x00,
    "temp": 0x01,
    "off": 0x02,
    "anim": 0x03,
    "graph": 0x04,
    "image": 0x05,
    "stopwatch": 0x06,
    "scoreboard": 0x07
}


def discover(ctx, lookup_known=True, spinner=click_spinner.Spinner()):
    if (lookup_known and len(KNOWN_DEVICES)):
        if (ctx.obj['debug']):
            click.echo('using knwon devices to find timebox')
        spinner.start()
        discovered = [(a, 'timebox') for a in KNOWN_DEVICES]
    else:
        if (ctx.obj['debug']):
            click.echo('scanning for timebox')
        spinner.start()
        discovered = bluetooth.discover_devices(duration=5, lookup_names=True)

    if (not len(discovered)):
        spinner.stop()
        click.echo("no devices discovered")
        ctx.abort()
    else:
        for a, n in discovered:
            if (n and 'timebox' in n.lower()):
                click.echo('checking device %s' % a)

                try:
                    sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
                    sock.connect((a, 6))
                    hello = [ord(c) for c in sock.recv(256)]

                    if(hello == TIMEBOX_HELLO):
                        ctx.obj['address'] = a
                        ctx.obj['sock'] = sock
                        break
                    else:
                        click.echo('invalid hello received')

                    sock.close()
                except bluetooth.BluetoothError as be:
                    pass
        spinner.stop()
        if (not 'address' in ctx.obj):
            if(len(KNOWN_DEVICES) and lookup_known):
                discover(ctx, False)
            else:
                click.echo('could not find a timebox ...')
                ctx.abort()
        else:
            if (ctx.obj['address'].upper() not in KNOWN_DEVICES and
                    click.confirm('would you like to add %s to known devices [y/n]?')):
                with open(CONFFILE, 'a') as f:
                    f.write(ctx.obj['address'].upper() + "\n")


@click.group()
@click.option('--address')
@click.option('--debug', is_flag=True)
@click.option('--disconnect', 'disconnect', flag_value=True, default=True)
@click.option('--keepconnected', 'disconnect', flag_value=False, default=True)
@click.pass_context
def cli(ctx, address, debug, disconnect):
    ctx.obj['debug']=debug

    if (not address):
        if(not os.path.exists(CONFDIR)):
            os.makedirs(CONFDIR)

        if(os.path.exists(CONFFILE)):
            with open(CONFFILE, 'r') as f:
                for l in f.readlines():
                    KNOWN_DEVICES.append(l.strip().upper())

        discover(ctx)
    else:
        ctx.obj['address'] = address

    if(debug):
        click.echo('connecting to %s' % ctx.obj['address'])

    try:
        if('sock' in ctx.obj):
            dev = connect(ctx.obj['sock'], debug)
        else:
            dev = connect(ctx.obj['address'], debug)
        ctx.obj['dev'] = dev

        return dev, disconnect
    except bluetooth.BluetoothError as be:
        click.echo('error connecting to %s\n%s' % (ctx.obj['address'], be))

    ctx.abort()


@cli.command(short_help='change view')
@click.argument('type', nargs=1) #, help="["+",".join(VIEWTYPES)+"]")
@click.pass_context
def view(ctx, type):
    if (type in VIEWTYPES):
        ctx.obj['dev'].send(switch_view(type))


@cli.command(short_help='display time')
@click.option('--color', nargs=1)
@click.option('--ampm', is_flag=True, help="12h format am/pm")
@click.pass_context
def clock(ctx, color, ampm):
    if (color):
        c = color_convert(Color(color).get_rgb())
        ctx.obj['dev'].send(set_time_color(c[0], c[1], c[2], 0xff, not ampm))
    else:
        ctx.obj['dev'].send(switch_view("clock"))


@cli.command(short_help='display temperature, set color')
@click.option('--color', nargs=1)
@click.option('--f', is_flag=True, help="fahrenheit")
@click.pass_context
def temp(ctx, color, f):
    if (color):
        c = color_convert(Color(color).get_rgb())
        ctx.obj['dev'].send(set_temp_color(c[0], c[1], c[2], 0xff, f))
    else:
        ctx.obj['dev'].send(switch_view("temp"))


def switch_view(type):
    h = [0x04, 0x00, 0x45, VIEWTYPES[type]]
    ck1, ck2 = checksum(sum(h))
    return [0x01] + mask(h) + mask([ck1, ck2]) + [0x02]


# 0x01 Start of message
# 0x02 End of Message
# 0x03 Mask following byte

def color_comp_conv(cc):
    cc = max(0.0, min(1.0, cc))
    return int(math.floor(255 if cc == 1.0 else  cc * 256.0))


def color_convert(rgb):
    return [color_comp_conv(c) for c in rgb]


def unmask(bytes, index=0):
    try:
        index = bytes.index(0x03, index)
    except ValueError:
        return bytes

    _bytes = bytes[:]
    _bytes[index + 1] = _bytes[index + 1] - 0x03
    _bytes.pop(index)
    return unmask(_bytes, index + 1)


def mask(bytes):
    _bytes = []
    for b in bytes:
        if (b == 0x01):
            _bytes = _bytes + [0x03, 0x04]
        elif (b == 0x02):
            _bytes = _bytes + [0x03, 0x05]
        elif (b == 0x03):
            _bytes = _bytes + [0x03, 0x06]
        else:
            _bytes += [b]

    return _bytes


def checksum(s):
    ck1 = s & 0x00ff
    ck2 = s >> 8

    return ck1, ck2


def set_time_color(r, g, b, x=0x00, h24=True):
    head = [0x09, 0x00, 0x45, 0x00, 0x01 if h24 else 0x00]
    s = sum(head) + sum([r, g, b, x])
    ck1, ck2 = checksum(s)

    # create message mask 0x01,0x02,0x03
    msg = [0x01] + mask(head) + mask([r, g, b, x]) + mask([ck1, ck2]) + [0x02]

    return msg


def set_temp_color(r, g, b, x, f=False):
    head = [0x09, 0x00, 0x45, 0x01, 0x01 if f else 0x00]
    s = sum(head) + sum([r, g, b, x])
    ck1, ck2 = checksum(s)

    # create message mask 0x01,0x02,0x03
    msg = [0x01] + mask(head) + mask([r, g, b, x]) + mask([ck1, ck2]) + [0x02]

    return msg

def set_temp_unit(f=False):
    head = [0x09, 0x00, 0x45, 0x01, 0x01 if f else 0x00]
    ck1, ck2 = checksum(sum(head))

    # create message mask 0x01,0x02,0x03
    msg = [0x01] + mask(head) + mask([ck1, ck2]) + [0x02]

    return msg


def analyseImage(im):
    '''
    Pre-process pass over the image to determine the mode (full or additive).
    Necessary as assessing single frames isn't reliable. Need to know the mode
    before processing all frames.
    '''
    results = {
        'size': im.size,
        'mode': 'full',
    }
    try:
        while True:
            if im.tile:
                tile = im.tile[0]
                update_region = tile[1]
                update_region_dimensions = update_region[2:]
                if update_region_dimensions != im.size:
                    results['mode'] = 'partial'
                    break
            im.seek(im.tell() + 1)
    except EOFError:
        pass
    im.seek(0)
    return results


def getFrames(im):
    '''
    Iterate the GIF, extracting each frame.
    '''
    mode = analyseImage(im)['mode']

    p = im.getpalette()
    last_frame = im.convert('RGBA')

    try:
        while True:
            '''
            If the GIF uses local colour tables, each frame will have its own palette.
            If not, we need to apply the global palette to the new frame.
            '''
            if not im.getpalette():
                im.putpalette(p)

            new_frame = Image.new('RGBA', im.size)

            '''
            Is this file a "partial"-mode GIF where frames update a region of a different size to the entire image?
            If so, we need to construct the new frame by pasting it on top of the preceding frames.
            '''
            if mode == 'partial':
                new_frame.paste(last_frame)

            new_frame.paste(im, (0, 0), im.convert('RGBA'))
            yield new_frame

            last_frame = new_frame
            im.seek(im.tell() + 1)
    except EOFError:
        pass


def process_image(imagedata, sz=11, scale=None):
    img = [0]
    bc = 0
    first = True

    if (scale):
        src = imagedata.resize((sz, sz), scale)
    else:
        src = imagedata.resize((sz, sz))

    for c in product(range(sz), range(sz)):
        y, x = c
        r, g, b, a = src.getpixel((x, y))

        if (first):
            img[-1] = ((r & 0xf0) >> 4) + (g & 0xf0) if a > 32 else 0
            img.append((b & 0xf0) >> 4) if a > 32 else img.append(0)
            first = False
        else:
            img[-1] += (r & 0xf0) if a > 32 else 0
            img.append(((g & 0xf0) >> 4) + (b & 0xf0)) if a > 32 else img.append(0)
            img.append(0)
            first = True
        bc += 1
    return img


def load_image(file, sz=11, scale=None):
    with Image.open(file).convert("RGBA") as imagedata:
        return process_image(imagedata, sz)


def load_gif_frames(file, sz=11, scale=None):
    with Image.open(file) as imagedata:
        for f in getFrames(imagedata):
            yield process_image(f, sz, scale)


def conv_image(data):
    # should be 11x11 px =>
    head = [0xbd, 0x00, 0x44, 0x00, 0x0a, 0x0a, 0x04]
    data = data
    ck1, ck2 = checksum(sum(head) + sum(data))

    msg = [0x01] + head + mask(data) + mask([ck1, ck2]) + [0x02]
    return msg


def prepare_animation(frames, delay=0):
    head = [0xbf, 0x00, 0x49, 0x00, 0x0a, 0x0a, 0x04]

    ret = []

    fi = 0
    for f in frames:
        _head = head + [fi, delay]
        ck1, ck2 = checksum(sum(_head) + sum(f))
        msg = [0x01] + mask(_head) + mask(f) + mask([ck1, ck2]) + [0x02]
        fi += 1
        ret.append(msg)

    return ret


@cli.command(short_help='display_image')
@click.argument('file', nargs=1)
@click.option('--scaling', type=click.Choice(FILTERS.keys()), default='bicubic')
@click.pass_context
def image(ctx, file, scaling):
    ctx.obj['dev'].send(conv_image(load_image(file, scale=FILTERS[scaling])))


@cli.command(short_help='display_animation')
@click.option('--gif', 'source', flag_value='gif')
@click.option('--folder', 'source', flag_value='folder', default=True)
@click.option('--delay', nargs=1)
@click.argument('path', nargs=1)
@click.option('--scaling', type=click.Choice(FILTERS.keys()), default='bicubic')
@click.pass_context
def animation(ctx, source, path, delay, scaling):
    frames = []

    if (source == "folder"):
        for f in listdir(path):
            f = join(path, f)
            if isfile(f):
                frames.append(load_image(f))
    elif (source == "gif"):
        for f in load_gif_frames(path, 11, scale=FILTERS[scaling]):
            frames.append(f)

    i = 0
    for f in prepare_animation(frames, delay=int(delay) if delay else 0):
        i = i + 1
        if(i==len(frames)):
            ctx.obj['dev'].send(f)
        else:
            ctx.obj['dev'].send(f, False)


# TODO: a bit weird, if the animation has "less frames than usual", it might be "glued" to the previous ;)
@cli.command(short_help='control fmradio')
@click.option('--on', 'state', flag_value=True, default=True)
@click.option('--off', 'state', flag_value=False)
@click.option('--frequency', nargs=1)
@click.pass_context
def fmradio(ctx, state, frequency):
    if (state):
        ctx.obj['dev'].send([0x01] + mask([0x04, 0x00, 0x05, 0x01, 0x0a, 0x00]) + [0x02])
        if (frequency):
            # TODO: WIP! setting frequency does not yet work as expected
            frequency = float(frequency)
            head = [0x05, 0x00]
            frac, whole = modf(frequency)
            frac = int(round(frac, 1) * 10)
            whole = (int(whole))
            f = [whole, frac]

            ff = (frac & 0xF0) + (whole << 8)
            print(ff)

            #print f
            #print mask(f)
            ck1, ck2 = checksum(sum(head) + sum(f))
            #print [ck1, ck2]
            #print mask([ck1, ck2])
            ctx.obj['dev'].send([0x01] + head + mask(f) + mask([ck1, ck2]) + [0x02])
    else:
        ctx.obj['dev'].send([0x01] + mask([0x04, 0x00, 0x05, 0x00, 0x09, 0x00]) + [0x02])


@cli.command(short_help='set volume')
@click.argument('level', nargs=1, type=click.IntRange(0, 16))
@click.pass_context
def volume(ctx, level):
    head = [0x04, 0x00, 0x08]
    ck1, ck2 = checksum(sum(head) + level)
    ctx.obj['dev'].send([0x01] + head + mask([level]) + mask([ck1, ck2]) + [0x02])


@cli.command(short_help='set time')
@click.argument('date', nargs=1)
@click.pass_context
def settime(ctx, date):
    if(date == "now"):
        dt = datetime.datetime.now()
    else:
        try:
            dt = dateutil.parser.parse(date)
        except:
            click.echo("could not parse date \"%s\"" % date)
            return

    head = [0x0A, 0x00, 0x18, dt.year%100, int(dt.year/100), dt.month, dt.day, dt.hour, dt.minute, dt.second ]
    s = sum(head)
    ck1, ck2 = checksum(s)
    ctx.obj['dev'].send([0x01]+mask(head)+mask([ck1,ck2])+[0x02])


@cli.command(short_help='raw message')
@click.option('--mask', '_mask', is_flag=True)
@click.argument('hexbytes', nargs=1)
@click.pass_context
def raw(ctx, hexbytes, _mask):
    if (_mask):
        ctx.obj['dev'].send(bytearray(mask(unhexlify(hexbytes))))
    else:
        ctx.obj['dev'].send(bytearray(unhexlify(hexbytes)))


def connect(target, debug):
    dev = Timebox(target, debug)
    dev.connect()

    return dev


if __name__ == '__main__':
    import sys
    dev, disconnect = cli(sys.argv[1:], obj={})
    if (disconnect):
        dev.disconnect()



Wichtig war hier, Port auf 1. Das 2mal.

self.sock.connect((self.addr, 1))

Und der nächste Port auf 6

sock.connect((a, 6))

In und Output

root@raspiFhem:/opt/fhem/divoom1/timebox# python timebox.py --debug --address 11:75:68:C9:7D:36 raw 010900720101010001007f0002
connecting to 11:75:68:C9:7D:36
-> ['01', '09', '00', '72', '01', '01', '01', '00', '01', '00', '7f', '00', '02']
<- ['01', '06', '00', '04', '72', '55', '01', 'd2', '00', '02']
root@raspiFhem:/opt/fhem/divoom1/timebox#

schwatter

#130
Und noch was gefunden. Wenn ich mit dem jetzigen Setup die Rawmessages durchgehe, bleibt das Script des
öfteren hängen. Schalte ich aber manuell an der Box die Funktionen durch, und komme zufällig auf die Funktion,
die zu der Rawmessage passt(nehme ich an), bekomme ich auch noch ein response und das Script läuft doch noch durch  ;D
Meißt passiert aber nichts an der Box. Wahrscheinlich muss dazu auf eine Funktion/Unterfunktion erst umgeschaltet werden.

Schaltet auf Stopuhr
root@raspiFhem:/opt/fhem/divoom1/timebox# python timebox.py --debug --address 11:75:68:C9:7D:36 raw 0104007100750002
connecting to 11:75:68:C9:7D:36
-> ['01', '04', '00', '71', '00', '75', '00', '02']
<- ['01', '07', '00', '04', '71', '55', '00', '00', 'd1', '00', '02']
root@raspiFhem:/opt/fhem/divoom1/timebox#


Farbe der Uhr geändert. So Weiß.
root@raspiFhem:/opt/fhem/divoom1/timebox# python timebox.py --debug --address 11:75:68:C9:7D:36 raw 010d004500010001000000ffffff510302
connecting to 11:75:68:C9:7D:36
-> ['01', '0d', '00', '45', '00', '01', '00', '01', '00', '00', '00', 'ff', 'ff', 'ff', '51', '03', '02']
<- ['01', '06', '00', '04', '45', '55', '00', 'a4', '00', '02']
root@raspiFhem:/opt/fhem/divoom1/timebox#


Farbe der Uhr geändert. So Blau.
root@raspiFhem:/opt/fhem/divoom1/timebox# python timebox.py --debug --address 11:75:68:C9:7D:36 raw 010d00450001000100000000e4ff370202
connecting to 11:75:68:C9:7D:36
-> ['01', '0d', '00', '45', '00', '01', '00', '01', '00', '00', '00', '00', 'e4', 'ff', '37', '02', '02']
<- ['01', '06', '00', '04', '45', '55', '00', 'a4', '00', '02']
root@raspiFhem:/opt/fhem/divoom1/timebox#


Farbe der Uhr geändert. So Grün.
root@raspiFhem:/opt/fhem/divoom1/timebox# python timebox.py --debug --address 11:75:68:C9:7D:36 raw 010d00450001000100000000ff05580102
connecting to 11:75:68:C9:7D:36
-> ['01', '0d', '00', '45', '00', '01', '00', '01', '00', '00', '00', '00', 'ff', '05', '58', '01', '02']
<- ['01', '06', '00', '04', '45', '55', '00', 'a4', '00', '02']
root@raspiFhem:/opt/fhem/divoom1/timebox#

mumpitzstuff

Also ich würde behaupten, das die Zeile mit dem Port 6 bei dir gar nicht ausgeführt wird. Wenn du debug angeschaltet hast, dann müsstest du die Ausgabe ,,Scanning for Timebox" sehen. Bei deinen geposteten Aufrufen wirkt sich diese Änderung meiner Meinung nach nicht aus. Du solltest durch die Verwendung von Port 1 in meinem Script die selben Dinge schalten können wie bei der Aurabox.

$socket->connect($device, 4)

ändern in

$socket->connect($device, 1)

mumpitzstuff

Das ändern der Farbe der Uhr sieht anders aus als bei Aurabox/Timebox. In der Mitte sind 5 Byte mehr zu sehen.

Früher hat das so ausgesehen:

450001FFFFFF

jetzt sieht das anscheinend so aus:

4500010001000000FFFFFF

Ist bei dir mehr als nur Stunden und Minuten zu sehen? Was passiert wenn du das verwendest:

4500010000000000FFFFFF

oder

4500010001FFFFFFFFFFFF

schwatter

Sobald ich die beiden Rawmessages per pyScript verschicke, bleibt es hängen.

010d004500010000000000FFFFFF
010d004500010001FFFFFFFFFFFF


Mehr als Std. und Minuten ist bei mir nicht zu sehen.


Mit Port 1 hatte ich schon getestet. Sowie aufsteigend bis 30.
Es bleibt bei Port 1 immer hängen. Schau dir den Code nochmal an,...bei dem ganzen hin und her
kann sich auch ein Bock eingeschlichen haben.

#!/usr/bin/perl
use strict;
use warnings;
use Time::HiRes;
use Net::Bluetooth;
use IO::Select;
use Imager;

sub listDevices();
sub connectDivoom($);
sub disconnectDivoom();
sub sendRaw($$;$);
sub sendPlain($$;$);
sub convertRawToPlain($);
sub convertImageTB($;$);
sub convertImageAB($;$);

my $socket;
my $TIMEBOX;

sub listDevices()
{
  print "Search for devices...\n\n"; 

  my $device_ref = get_remote_devices();
 
  foreach my $addr (keys %$device_ref)
  {
    print "Address: $addr Name: $device_ref->{$addr}\n";
  }

  print "done\n\n";
}

sub connectDivoom($)
{
  my $device = shift;
  my $ret;
  my $success = 0;

  print "Create RFCOMM client ($device)...\n";

  $socket = Net::Bluetooth->newsocket("RFCOMM");
  return $success unless(defined($socket));
 
  if (0 != $socket->connect($device, 1))
  {
    $socket->close();
    return $success;
  }

  $TIMEBOX = $socket->perlfh();
 
  sysread($TIMEBOX, $ret, 256);
  if (defined($ret))
  {
    $ret =~ s/[^[:print:]]//g;
    print "Device answer: $ret";

    #if ('HELLO' eq $ret)
    if ('Up' eq $ret)
    {
      $success = 1;
    }
    else
    {
      close($TIMEBOX);
      $socket->close();
    }
  }
 
  print "\ndone\n\n";

  return $success;
}

sub disconnectDivoom()
{
  close($TIMEBOX);
  $socket->close();
}

sub sendRaw($$;$)
{
  my $data = shift;
  my $timeout = shift;
  my $response = shift;
  my $ret;
  my $retry = 0;
  my $select = IO::Select->new($TIMEBOX);
 
  print "Send raw command: $data\n";

  $response = 1 if (!defined($response));

  $data =~ s/((?:[0-9a-fA-F]{2})+)/pack('H*', $1)/ge;
 
  do
  {
    syswrite($TIMEBOX, $data);

    if ($select->can_read(0.1))
    {
      sysread($TIMEBOX, $ret, 256);
      if (defined($ret))
      {
        $ret = unpack('(H2)*', $ret);
        $ret =~ s/[^[:print:]]+//g;
        print "Device answer: $ret\n";
      }
    }
    else
    {
      print "No answer from device!\n";
    }

    $retry++;
  } while (($response) && ($retry <= 3) && (!defined($ret) || '01' ne $ret));

  if ($retry > 3)
  {
    print "Failed!\n";
  }
  else
  {
    Time::HiRes::sleep($timeout);
  }

  print "done\n\n";
}

sub sendPlain($$;$)
{
  my $data = shift;
  my $timeout = shift;
  my $response = shift;
  my $crc = 0;
  my $ret;
  my $retry = 0;

  print "Send plain command: $data\n";

  # add length (length of data + length of checksum)
  $_ = (length($data) + 4) / 2;
  $data = sprintf("%02x", ($_ & 0xFF)).sprintf("%02x", (($_ >> 8) & 0xFF)).$data;

  # calculate crc
  while ($data =~ /(..)/g)
  {
    $crc += hex($1);
  }

  # add crc
  $data .= sprintf("%02x", ($crc & 0xFF)).sprintf("%02x", (($crc >> 8) & 0xFF)); 

  # escape data
  $data =~ s/(01|02|03)(?{ if (0 == ($-[0] & 1)) {'030'.(3+$1)} else {$1} })/$^R/g;

  # add prefix and postfix
  $data = '01'.$data.'02';

  print "Generated raw command: $data\n";

  sendRaw($data, $timeout, $response);
}

sub convertRawToPlain($)
{
  my $data = shift;

  print $data."\n";

  # remove prefix and postfix
  $data = substr($data, 2, -2);

  # unescape data
  $data =~ s/(03(04|05|06))(?{ if (0 == ($-[0] & 1)) {'0'.($2-3)} else {$1} })/$^R/g;
 
  #remove length
  $data = substr($data, 4);

  # remove checksum
  $data = substr($data, 0, -4);

  print $data."\n";

  return $data;
}

sub convertImageTB($;$)
{
  my $file = shift;
  my $size = shift;
  my @imgData = (0);
  my $image = Imager->new;
 
  $size = 11 if (!defined($size));
  $image->read(file=>$file) or die "Can't read image ".$file." (".$image->errstr.")\n";
 
  if ('paletted' eq $image->type)
  {
    print "Image: ".$image->getheight()."x".$image->getwidth()." (maxcolors: ".$image->maxcolors.", usedcolors: ".$image->getcolorcount().")\n";
  }
  else
  {
    print "Image: ".$image->getheight()."x".$image->getwidth()." (maxcolors: no palette found, usedcolors: ".$image->getcolorcount().")\n";
  }

  if (defined($image))
  {
    my ($r, $g, $b, $a);
    my $flicflac = 0;   
    my $imageResized = $image->scaleX(pixels=>$size)->scaleY(pixels=>$size);

    for (my $y = 0; $y < $size; $y++)
    {
      for (my $x = 0; $x < $size; $x++)
      {
        ($r, $g, $b, $a) = $imageResized->getpixel(x=>$x, y=>$y)->rgba();
       
        if (0 == $flicflac)
        {
          if ($a > 32)
          {
            $imgData[-1] = (($r & 0xF0) >> 4) + ($g & 0xF0);
            push(@imgData, (($b & 0xF0) >> 4));
          }
          else
          {
            $imgData[-1] = 0;
            push(@imgData, 0);
          }

          $flicflac = 1;
        }
        else
        {
          if ($a > 32)
          {
            $imgData[-1] += ($r & 0xF0);
            push(@imgData, (($g & 0xF0) >> 4) + ($b & 0xF0));
          }
          else
          {
            $imgData[-1] += 0;
            push(@imgData, 0);
          }
          push(@imgData, 0);

          $flicflac = 0;
        }
      }
    }
  }
  else
  {
    print "Error: Loading image failed!\n";
  }

  $_ = '';
  foreach my $byte (@imgData)
  {
    $_ .= sprintf("%02x", ($byte & 0xFF));
  }

  return $_;
}

sub convertImageAB($;$)
{
  my $file = shift;
  my $size = shift;
  my @imgData = ();
  my $image = Imager->new;
  my @color = (0, 1, 2, 11, 4, 5, 2, 5, 8, 1, 2, 3, 4, 13, 6, 7);

  $size = 10;# if (!defined($size));
  $image->read(file=>$file) or die "Can't read image ".$file." (".$image->errstr.")\n";

  if ('paletted' eq $image->type)
  {
    print "Image: ".$image->getheight()."x".$image->getwidth()." (maxcolors: ".$image->maxcolors.", usedcolors: ".$image->getcolorcount().")\n";
  }
  else
  {
    print "Image: ".$image->getheight()."x".$image->getwidth()." (maxcolors: no palette found, usedcolors: ".$image->getcolorcount().")\n";
  }

  if (defined($image))
  {
    my $flicflac = 0;
    #my $imageResized = $image->scaleX(pixels=>$size)->scaleY(pixels=>$size);

    for (my $y = 0; $y < $size; $y++)
    {
      for (my $x = 0; $x < $size; $x++)
      {
        my $index = $image->findcolor(color=>$image->getpixel(x=>$x, y=>$y));
        print "Warning: palette index (".$index.") outside of allowed range at x=".$x." y=".$y."\n" if ($index > 15);
        $index = $index % 16;
               
        if (0 == $flicflac)
        {
          push(@imgData, $color[$index]);

          $flicflac = 1;
        }
        else
        {
          $imgData[-1] += ($color[$index] << 4);

          $flicflac = 0;
        }
      }
    }
  }
  else
  {
    print "Error: Loading image failed!\n";
  }

  $_ = '';
  foreach my $byte (@imgData)
  {
    $_ .= sprintf("%02x", ($byte & 0xFF));
  }

  return $_;
}

if (connectDivoom('11:75:68:C9:7D:36'))
{
  sendRaw('0104007100750002', 5);
  sendRaw('010d004500010001000000ffffff510302', 5);
  sendRaw('010d00450001000100000000e4ff370202', 5);
  sendRaw('010d00450001000100000000ff05580102', 5);
  disconnectDivoom();
}

mumpitzstuff

if (connectDivoom('11:75:68:C9:7D:36'))
{
  sendPlain('7100', 5);
  sendPlain('4500010001000000ffffff', 5);
  sendPlain('450001000100000000e4ff', 5);
  sendPlain('450001000100000000ff05', 5);
  disconnectDivoom();
}


Probier es mal damit. Bei den RAW Nachrichten gibt es einen Unterschied. Ich erwarte hier bereits das 01-03 escaped sind. Bei dem Python Script wird der Datenstrom intern escaped. In meine Funktion kannst du genau das was du in deinen Bluetooth Logs siehst eingeben und musst das nicht erst unescapen. Ich kann aber auch escape oder nicht escape automatisch in der Funktion machen wenn erforderlich. Hatte dafür bisher keine Notwendigkeit gesehen.