diff --git a/fhem/CHANGED b/fhem/CHANGED index 990e81548..e1f5809ac 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - new: 00_DFPlayerMini: module to control an embedded MP3 player - feature: 75_MSG: add support for PostMe module - feature: 74_XiaomiFlowerSens: 1.0.1 new Attributs minLux and maxLux - feature: 98_monitoring: blacklist attribute is now a space seperated list diff --git a/fhem/FHEM/00_DFPlayerMini.pm b/fhem/FHEM/00_DFPlayerMini.pm new file mode 100644 index 000000000..c60d5a38c --- /dev/null +++ b/fhem/FHEM/00_DFPlayerMini.pm @@ -0,0 +1,1994 @@ +############################################## +# $Id$ +# +# Support for the "FN-M16P Embedded MP3 Audio Module" aka DFPlayer Mini +# (http://www.flyrontech.com/eproducts/84.html) +# It can be connected directly +# via serial port @ 9600 baud or via TCP/IP with a transparent serial bridge like ESPEasy +# This seems to be the real and most complete datasheet +# http://www.flyrontech.com/edownload/6.html +# see also https://www.dfrobot.com/wiki/index.php/DFPlayer_Mini_SKU:DFR0299 +# and http://forum.banggood.com/forum-topic-59997.html + + +package main; + +use strict; +use warnings; +use Time::HiRes qw(gettimeofday); +use Data::Dumper qw(Dumper); +use Scalar::Util qw(looks_like_number); +use Cwd 'abs_path'; +use File::Spec::Functions qw(splitpath catfile); +use File::Copy; +use Digest::MD5 qw(md5_hex); + +use constant { + DFP_INIT_WAIT => 2, + DFP_INIT_MAXRETRY => 3, + DFP_CMD_TIMEOUT => 10, + DFP_KEEPALIVE_TIMEOUT => 60, + DFP_KEEPALIVE_MAXRETRY => 3, + DFP_MIN_WAITTIME => 0.1, # 0.02, +}; + +use constant { + DFP_Start_Byte => 0x7E, + DFP_Version_Byte => 0xFF, + DFP_Command_Length => 0x06, + DFP_End_Byte => 0xEF, + DFP_Acknowledge => 0x01, # For each command an answer is sent back + DFP_NoAcknowledge => 0x00, + DFP_FrameLength => 10, + + # Equalizermodes + DFP_EQ_Normal => 0, + DFP_EQ_Pop => 1, + DFP_EQ_Rock => 2, + DFP_EQ_Jazz => 3, + DFP_EQ_Classic => 4, + DFP_EQ_Bass => 5, + + # Playbacksources + DFP_PS_USB => 0, + DFP_PS_SD => 1, + + # Errorcodes + DFP_E_Busy => 1, + DFP_E_Sleeping => 2, + DFP_E_Receive => 3, + DFP_E_Checksum => 4, + DFP_E_TrackRange => 5, + DFP_E_TrackNotFound => 6, + DFP_E_Intercut => 7, + DFP_E_SDRead => 8, + DFP_E_EnteredSleep => 0x0a, + + + # Commands + DFP_C_Next => 0x01, + DFP_C_Previous => 0x02, + DFP_C_TrackNum => 0x03, # 0-2999 + DFP_C_IncreaseVolume => 0x04, + DFP_C_DecreaseVolume => 0x05, + DFP_C_SetVolume => 0x06, # 0-30 + DFP_C_SetEqualizerMode => 0x07, + DFP_C_SetRepeatSingle => 0x08, + DFP_C_SetStorage => 0x09, + DFP_C_Sleep => 0x0a, # DFP responses only with Busy and requires a power cycle + DFP_C_Wake => 0x0b, # only supported by FN-M22P + DFP_C_Reset => 0x0c, + DFP_C_Play => 0x0d, + DFP_C_Pause => 0x0e, + DFP_C_SetPlaybackFolder => 0x0f, # 1-99 + DFP_C_Amplification => 0x10, + DFP_C_RepeatAllRoot => 0x11, + DFP_C_SetPlaybackFolderMP3 => 0x12, + DFP_C_IntercutAdvert => 0x13, + DFP_C_SetPlaybackFolder3000 => 0x14, # 1-15 + DFP_C_StopAdvert => 0x15, + DFP_C_Stop => 0x16, + DFP_C_RepeatFolder => 0x17, + DFP_C_Shuffle => 0x18, + DFP_C_RepeatCurrent => 0x19, + DFP_C_SetDAC => 0x1a, + + # Query Commands and responses + DFP_C_StoragePluggedIn => 0x3a, + DFP_C_StoragePulledOut => 0x3b, + DFP_C_TrackFinishedUSB => 0x3c, + DFP_C_TrackFinishedSD => 0x3d, + DFP_C_GetStorage => 0x3f, + DFP_C_Acknowledge => 0x41, + DFP_C_Error => 0x40, + DFP_C_GetStatus => 0x42, + DFP_C_GetVolume => 0x43, + DFP_C_GetEqualizerMode => 0x44, + DFP_C_GetNoTracksRootUSB => 0x47, + DFP_C_GetNoTracksRootSD => 0x48, + DFP_C_GetCurrentTrackUSB => 0x4b, + DFP_C_GetCurrentTrackSD => 0x4c, + DFP_C_GetNoTracksInFolder => 0x4e, + DFP_C_GetNoFolders => 0x4f, +}; + +my %errorTexts = ( + &DFP_E_Busy => "Busy", + &DFP_E_Sleeping => "Sleeping", + &DFP_E_Receive => "Receive Error", + &DFP_E_Checksum => "Checksum Error", + &DFP_E_TrackRange => "Track out of range", + &DFP_E_TrackNotFound => "Track not found", + &DFP_E_Intercut => "Intercut not possible", + &DFP_E_SDRead => "SD card read failed", + &DFP_E_EnteredSleep => "Entered sleep mode", +); + +my %statusStorageTexts = ( + 0 => "no storage", + 1 => "USB", + 2 => "SD", + 4 => "connected to PC", + 16 => "Sleep mode", +); +my %statusModeTexts = ( + 0 => "stopped", + 1 => "playing", + 2 => "paused", +); +my %equalizerTexts = ( + &DFP_EQ_Normal => "Normal", + &DFP_EQ_Pop => "Pop", + &DFP_EQ_Rock => "Rock", + &DFP_EQ_Jazz => "Jazz", + &DFP_EQ_Classic => "Classic", + &DFP_EQ_Bass => "Bass", +); + +sub DFPlayerMini_Attr(@); +sub DFPlayerMini_HandleWriteQueue($); +sub DFPlayerMini_Parse($$); +sub DFPlayerMini_Read($); +sub DFPlayerMini_Ready($); +sub DFPlayerMini_Write($$$); +sub DFPlayerMini_SimpleWrite(@); + +my %gets = ( # Name, Data to send to the DFPlayer Mini, Regexp for the answer + "storage" => [ DFP_C_GetStorage, 'noArg' ], + "status" => [ DFP_C_GetStatus, 'noArg' ], + "volume" => [ DFP_C_GetVolume, 'noArg' ], + "equalizer" => [ DFP_C_GetEqualizerMode, 'noArg' ], + "noTracksRootUsb" => [ DFP_C_GetNoTracksRootUSB, 'noArg'], + "noTracksRootSd" => [ DFP_C_GetNoTracksRootSD, 'noArg'], + "currentTrackUsb" => [ DFP_C_GetCurrentTrackUSB, 'noArg'], + "currentTrackSd" => [ DFP_C_GetCurrentTrackSD, 'noArg'], + "noTracksInFolder" => [ DFP_C_GetNoTracksInFolder, ""], + "noFolders" => [DFP_C_GetNoFolders, 'noArg'], +); + + +my %sets = ( + # corresponding to DFP commands + "next" => 'noArg', + "prev" => 'noArg', + "trackNum" => "", + "volumeUp" => "noArg", + "volumeDown" => "noArg", + "volumeStraight" => 'slider,0,1,30', + "equalizer" => join(",", values(%equalizerTexts)), + "repeatSingle" => '', + "storage" => 'USB,SD', + "sleep" => 'noArg', + "wake" => 'noArg', + "reset" => "noArg", + "play" => "", + "pause" => 'noArg', + "amplification" => "slider,0,1,31", + "repeatRoot" => 'on,off', + "MP3TrackNum" => "", + "intercutAdvert" => "", + "folderTrackNum" => "", + "folderTrackNum3000" => "", + "stopAdvert" => 'noArg', + "stop" => 'noArg', + "repeatFolder" => '', + "shuffle" => 'noArg', + "repeatCurrentTrack" => 'on,off', + "DAC" => "on,off", + # helper commands + "close" => "noArg", + "raw" => "", + "uploadTTS" => "", + "uploadTTScache" => "", + "uploadNumbers" => "", + "sayNumber" => "", + "readFiles" => "noArg", + "response" => "", + "reopen" => "noArg", + "tts" => "", + #"playlist" => "noArg", +); + +sub +DFPlayerMini_getKeyByValue($$) { + my ($hash, $val) = $@; + +} + + +sub +DFPlayerMini_Initialize($) +{ + my ($hash) = @_; + + require "$attr{global}{modpath}/FHEM/DevIo.pm"; + +# Provider + $hash->{ReadFn} = "DFPlayerMini_Read"; + $hash->{WriteFn} = "DFPlayerMini_Write"; + $hash->{ReadyFn} = "DFPlayerMini_Ready"; + +# Normal devices + $hash->{DefFn} = "DFPlayerMini_Define"; + $hash->{FingerprintFn} = "DFPlayerMini_FingerprintFn"; + $hash->{UndefFn} = "DFPlayerMini_Undef"; + $hash->{GetFn} = "DFPlayerMini_Get"; + $hash->{SetFn} = "DFPlayerMini_Set"; + $hash->{AttrFn} = "DFPlayerMini_Attr"; + $hash->{NotifyFn} = "DFPlayerMini_Notify"; + $hash->{AttrList} = "requestAck:0,1 TTSDev uploadPath sendCmd keepAliveInterval" + . " rememberMissingTTS:0,1 do_not_notify:1,0 " + ." $readingFnAttributes"; + + $hash->{ShutdownFn} = "DFPlayerMini_Shutdown"; + $hash->{parseParams} = 1; + + $hash->{PLAYQUEUE} = (); + $hash->{TTSQUEUE} = (); + +} + +sub +DFPlayerMini_FingerprintFn($$) +{ + my ($name, $msg) = @_; + + return ($name, $msg); +} + +##################################### +sub +DFPlayerMini_Define($$) +{ + my ($hash, $a, $h) = @_; + + if(int(@$a) != 3) { + my $msg = "wrong syntax: define DFPlayerMini {none | devicename[\@baudrate] | devicename\@directio | hostname:port}"; + Log3 undef, 2, $msg; + return $msg; + } + + DevIo_CloseDev($hash); + my $name = @$a[0]; + + + my $dev = @$a[2]; + + + if ($dev ne "none" && $dev =~ m/[a-zA-Z]/ && $dev !~ m/\@/) { # bei einer IP wird kein \@9600 angehaengt + $dev .= "\@9600"; + } + + $hash->{DeviceName} = $dev; + + my $ret=undef; + + if($dev ne "none") { + $ret = DevIo_OpenDev($hash, 0, "DFPlayerMini_DoInit", 'DFPlayerMini_Connect'); + + } else { + $hash->{DevState} = 'initialized'; + readingsSingleUpdate($hash, "state", "opened", 1); + } + $hash->{NOTIFYDEV} = "global"; + + + $hash->{LAST_SEND_TS}=0; + $hash->{LAST_RECV_TS}=0; + $hash->{helper}{LAST_RESPONSE} = ""; + + if ($init_done) { + $attr{$name}{cmdIcon} = "volumeDown:rc_VOLMINUS volumeUp:rc_VOLPLUS prev:rc_PREVIOUS play:rc_PLAY next:rc_NEXT pause:rc_PAUSE stop:rc_STOP"; + $attr{$name}{webCmd} = "volumeStraight:volumeDown:volumeUp:prev:play:next:pause:stop"; + $attr{$name}{icon} = "audio_audio"; + } + return $ret; +} + +############################### +sub DFPlayerMini_Connect($$) +{ + my ($hash, $err) = @_; + + # damit wird die err-msg nur einmal ausgegeben + if (!defined($hash->{disConnFlag}) && $err) { + Log3($hash, 3, "DFPlayerMini $hash->{NAME}: ${err}"); + $hash->{disConnFlag} = 1; + } +} + +##################################### +sub +DFPlayerMini_Undef($$) +{ + my ($hash, $arg) = @_; + my $name = $hash->{NAME}; + + foreach my $d (sort keys %defs) { + if(defined($defs{$d}) && + defined($defs{$d}{IODev}) && + $defs{$d}{IODev} == $hash) + { + my $lev = ($reread_active ? 4 : 2); + Log3 $name, $lev, "$name: deleting port for $d"; + delete $defs{$d}{IODev}; + } + } + + DFPlayerMini_Shutdown($hash); + + DevIo_CloseDev($hash); + RemoveInternalTimer($hash); + return undef; +} + +##################################### +sub +DFPlayerMini_Shutdown($) +{ + my ($hash) = @_; + return undef; +} + + +##################################### +sub +DFPlayerMini_createCmd($$;$$) +{ + my ($hash, $cmd, $par1, $par2) = @_; + + $par1 = 0 if !defined $par1; + $par2 = 0 if !defined $par2; + + my $requestAck = AttrVal($hash->{NAME}, "requestAck", 0) || $cmd == DFP_C_Acknowledge; + + my $checksum = -(DFP_Version_Byte+DFP_Command_Length+$cmd+$requestAck+$par1+$par2); + return pack('CCCCCCCnC', DFP_Start_Byte,DFP_Version_Byte, DFP_Command_Length, $cmd, $requestAck, $par1, $par2, $checksum, DFP_End_Byte); + +} + + +##################################### +sub +DFPlayerMini_uploadTTScache($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + my $ttsDev = AttrVal($name, "TTSDev", ""); + my $uploadPath = AttrVal($name, "uploadPath", ""); + + return "please set attribute TTSDev to a valid Text2Speech device" if $ttsDev eq ""; + return "please set attribute uploadPath to root directory of the SD card/USB stick the sound files should be uploaded to" if $uploadPath eq ""; + return "$uploadPath doesn't exist" if !-e $uploadPath; + return "$uploadPath must be a directory" if !-d $uploadPath; + return "$uploadPath must be writable" if !-x $uploadPath; + + my $TTS_CacheFileDir = AttrVal($ttsDev, "TTS_CacheFileDir", "cache"); + my $noTracks = 0; + my $folder = 1; + my $srcFile; + my $destDir; + my $destFile; + my $md5; + if (opendir CACHE, $TTS_CacheFileDir) { + while ($srcFile = readdir CACHE) { + next if $srcFile eq '.' or $srcFile eq '..'; + $noTracks++; + if ($noTracks > 3000) { + $noTracks = 0; + $folder++; + } + last if $folder > 15; # return "too many files in $TTS_CacheFileDir, stopping after " . 15*3000 + + $destDir = catfile($uploadPath, sprintf("%02d", $folder)); + if (!-e $destDir) { + mkdir $destDir, 511 or return "failed to create directory $destDir: $!"; + } + + $destFile = catfile($destDir, sprintf("%04dMD5%s", $noTracks, $srcFile)); + $srcFile = catfile($TTS_CacheFileDir, $srcFile); + $md5= $srcFile; + $md5 =~ s/.mp3$//; + delete $hash->{READINGS}{"Missing_MD5$md5"}; + Log3 $name, 4, "$name: cp $srcFile $destFile"; + copy($srcFile, $destFile); + } + closedir CACHE; + } + + return undef; +} + +##################################### +sub +DFPlayerMini_uploadNumbers($$) +{ + my ($hash, $destDir) = @_; + my $name = $hash->{NAME}; + + #my @terms = qw(ein zwei drei); + my @terms = qw(null ein zwei drei vier fünf sechs sieben acht neun zehn elf zwölf + zwanzig dreissig vierzig fünfzig sechzig siebzig achtzig neunzig hundert + sechzehn siebzehn und hundert tausend million millionen komma minus); + + my $i=1; + foreach my $term (@terms) { + my $filename = "${i}${term}"; + $i++; + Log3 $name, 5, "$name: calling uploadTTS " . catfile($destDir, $filename) . " $term"; + my $ret = DFPlayerMini_uploadTTS($hash, catfile($destDir, $filename), $term); + return $ret if $ret; + } + return ""; +} + +##################################### +# taken from Lingua::DE::Num2Word (http://search.cpan.org/~rvasicek/Lingua-DE-Num2Word-0.03/Num2Word.pm) +sub DFPlayerMini_num2de_cardinal { + my $positive = shift; + + my @tokens1 = qw(null ein zwei drei vier fünf sechs sieben acht neun zehn elf zwölf); + my @tokens2 = qw(zwanzig dreissig vierzig fünfzig sechzig siebzig achtzig neunzig hundert); + + return $tokens1[$positive] if($positive >= 0 && $positive < 13); # 0 .. 12 + return 'sechzehn' if($positive == 16); # 16 exception + return 'siebzehn' if($positive == 17); # 17 exception + return ($tokens1[$positive-10], 'zehn') if($positive > 12 && $positive < 20); # 13 .. 19 + + my @out = (); # string for return value construction + my $one_idx; # index for tokens1 array + my $remain; # remainder + + if($positive > 19 && $positive < 101) { # 20 .. 100 + $one_idx = int ($positive / 10); + $remain = $positive % 10; + + push @out, ($tokens1[$remain], "und") if $remain; + push @out, $tokens2[$one_idx-2]; + + } elsif($positive > 100 && $positive < 1000) { # 101 .. 999 + $one_idx = int ($positive / 100); + $remain = $positive % 100; + + push @out, ($tokens1[$one_idx], "hundert"); + push @out, $remain ? &DFPlayerMini_num2de_cardinal($remain) : ''; + + } elsif($positive > 999 && $positive < 1_000_000) { # 1000 .. 999_999 + $one_idx = int ($positive / 1000); + $remain = $positive % 1000; + + push @out, (&DFPlayerMini_num2de_cardinal($one_idx), 'tausend'); + push @out, $remain ? &DFPlayerMini_num2de_cardinal($remain) : ''; + + } elsif($positive > 999_999 && + $positive < 1_000_000_000) { # 1_000_000 .. 999_999_999 + $one_idx = int ($positive / 1000000); + $remain = $positive % 1000000; + my $one = $one_idx == 1 ? 'e' : ''; + + push @out, (&DFPlayerMini_num2de_cardinal($one_idx), $one); + push @out, $one_idx > 1 ? "millionen" : "million"; + if ($remain) { + push @out, &DFPlayerMini_num2de_cardinal($remain); + } + } + + return @out; +} + +##################################### +sub +DFPlayerMini_sayNumber($$) +{ + my ($hash, $number) = @_; + + if ($number =~ /(^-?\d+)\.?(\d*)$/) { + my $intpart = $1; + my $decpart = $2; + + my @terms; + + if ($intpart < 0) { + push @terms, "minus" ; + $intpart *= -1; + } + push @terms, DFPlayerMini_num2de_cardinal($intpart); + if ($decpart) { + push @terms, "komma"; + for (my $i=0; $i{NAME}; + + my $ttsDev = AttrVal($name, "TTSDev", ""); + return "please enter a text to be translated to speech" if $ttsSay eq ""; + return "please set attribute TTSDev to a valid Text2Speech device" if $ttsDev eq ""; + + my $ttsCmd = "! " . $ttsSay; + push @{$hash->{TTSQUEUE}}, $ttsCmd; + $hash->{LAST_TTS} = $ttsSay; + if (ReadingsVal($ttsDev, "playing", 1)) { + # tts currently going on. As Text2Speech has no queue we create an own one + # to avoid interrupting an ongoing tts operation + Log3 $name, 4, "$name: tts busy, queueing $ttsCmd"; + $hash->{TTS_BUSY} = 1; + return; + } + fhem "set $ttsDev tts " . $ttsSay; + +} + +##################################### +sub +DFPlayerMini_uploadTTS($$$) +{ + my ($hash, $destFile, $ttsSay) = @_; + my $name = $hash->{NAME}; + my $ttsDev = AttrVal($name, "TTSDev", ""); + my $uploadPath = AttrVal($name, "uploadPath", ""); + my $maxTracks = 0; + my $trackNum = 0; + my $trackName = ""; + + + + return "please enter a text to be translated to speech" if $ttsSay eq ""; + return "please set attribute TTSDev to a valid Text2Speech device" if $ttsDev eq ""; + return "please set attribute uploadPath to root directory of the SD/USB the sound files should be uploaded to" if $uploadPath eq ""; + return "$uploadPath doesn't exist" if !-e $uploadPath; + return "$uploadPath must be a directory" if !-d $uploadPath; + return "$uploadPath must be writable" if !-x $uploadPath; + + + # 01/1Test + # MP3/003Song + my ($destvol, $dirname, $filename) = splitpath($destFile); + + $dirname =~ s/(\\|\/)$//; # remove trailing / or \ + $dirname = uc($dirname); + + if ($dirname eq "MP3") { + $maxTracks = 65536; + } elsif ($dirname eq "ADVERT") { + $maxTracks = 3000; + } elsif ($dirname =~ /^\d{1,2}$/) { + my $folderNum = int($dirname); + + return "folder must be between 1 and 99" if $folderNum < 1; + + if ($folderNum <= 15) { + $maxTracks = 3000; + } else { + $maxTracks = 255; + } + $dirname = sprintf("%02d", $folderNum); + } elsif ($dirname eq ".") { + $maxTracks = 99; + } else { + return "$dirname must be either MP3, ADVERT, . or a number between 1 and 99"; + } + if ($filename =~ /(\d{1,5})(.*)/) { + $trackNum = $1; + $trackName = $2; + if ($trackNum < 1 || $trackNum > $maxTracks) { + return "track number $trackNum must be between 1 and $maxTracks"; + } + } else { + return "$destFile filename must start with digits but no more than 5"; + } + + my $digits = length("$maxTracks"); + my $formattedTrackNum = sprintf("%0${digits}d", $trackNum); + $destFile = $formattedTrackNum . $trackName; + + my $destDir = catfile($uploadPath, $dirname); + + + if (!-e $destDir) { + mkdir $destDir, 511 or return "failed to create directory $destDir: $!"; + } + + # delete all files which start with the same number as the current track as the DFP identifies a file only by the number + my $deletePattern = catfile($destDir, $formattedTrackNum) . '*'; + Log3 $name, 5, "$name: deleting: $deletePattern"; + unlink glob($deletePattern); + + + $destFile = catfile($dirname, $destFile); + Log3 $name, 5, "$name: destFile = $destFile ttsSay = $ttsSay"; + my $ttsCmd = $destFile . " " . $ttsSay; + push @{$hash->{TTSQUEUE}}, $ttsCmd; + if (ReadingsVal($ttsDev, "playing", 1)) { + # tts currently going on. As Text2Speech has no queue we create an own one + # to avoid interrupting an ongoing tts operation + Log3 $name, 4, "$name: tts busy, queueing $ttsCmd"; + $hash->{TTS_BUSY} = 1; + return; + } + + + + fhem "set $ttsDev tts " . $ttsSay; + + # when tts is done, DFPlayerMini_Notify will be called + + return undef; +} + +##################################### +sub +DFPlayerMini_readFiles($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + my $fileNo; + my $fileName; + my $file; + my @allfiles; + + my $path = AttrVal($hash->{NAME}, "uploadPath", ""); + return "please set attribute uploadPath" if $path eq ""; + + my @dirs = ("MP3", "ADVERT"); + for my $count (1..99) { + push @dirs, (sprintf("%02d",$count)); + } + + foreach my $dir (@dirs) { + #Log3 $name, 4, "$name: reading $dir..."; + my $mp3dir = catfile($path, $dir); + if (opendir MP3DIR, $mp3dir) { + push @allfiles, map "File_$dir/$_", grep /[0-9]{3,4}.*\.(mp3|wav)/, readdir MP3DIR; + closedir MP3DIR; + } else { + #Log3 $name, 4, "$name: can't open $mp3dir"; + } + } + readingsBeginUpdate($hash); + foreach my $reading (grep /File_.*\/[0-9]{3,4}/, keys %{$hash->{READINGS}}) { + #Log3 $name, 4, "$name: deleting $reading"; + delete($hash->{READINGS}{$reading}); + } + + foreach $file (@allfiles) { + #Log3 $hash, 5, "$name: file $file"; + my ($fileNo, $fileName) = ($file =~ /(File_.*\/[0-9]{3,4})(.*)\.(mp3|wav)/); + readingsBulkUpdate($hash, $fileNo, $fileName); + } + readingsEndUpdate($hash,0); + + return undef; + +} + +##################################### +sub DFPlayerMini_Play($@) +{ + my ($hash, @args) = @_; + my $name = $hash->{NAME}; + my $found = 0; + my $r; + + foreach my $arg (@args) { + Log3 $name, 5, "$name: playing $arg"; + if ($arg !~ /\//) { + # not a reading name (which must contain a /), search the reading values + + if (ReadingsVal($name, "advertPossible", 0) == 1) { + # first try to play as advert + foreach $r (grep /^File_ADVERT/, keys %{$hash->{READINGS}}) { + #Log3 $name, 5, "$name: testing $r " . $hash->{READINGS}{$r}; + if ($hash->{READINGS}{$r}{VAL} eq $arg ) { + $arg = $r; + $found = 1; + last; + } + } + } + if (!$found) { + foreach $r (grep /^File_[^A]/, keys %{$hash->{READINGS}}) { + #Log3 $name, 5, "$name: testing $r " . $hash->{READINGS}{$r}; + if ($hash->{READINGS}{$r}{VAL} eq $arg ) { + $arg = $r; + last; + } + } + } + } + my $playArg = ReadingsVal($name, $arg, undef); + if (!defined $playArg) { + $playArg = ReadingsVal($name, "File_$arg", undef); + } + if (defined $playArg) { + my ($fileMarker, $path) = split(/_/, $arg, 2); + my ($folder, $track) = split(/\//, $path, 2); + $folder = uc($folder); + Log3 $name, 2, "$name: path $path folder $folder track $track"; + if ($folder eq "MP3") { + DFPlayerMini_AddToPlayQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_SetPlaybackFolderMP3, $track >> 8, $track & 0xff), 0); + } elsif ($folder eq "ADVERT") { + DFPlayerMini_AddToPlayQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_IntercutAdvert, $track >> 8, $track & 0xff), 1); + } elsif (looks_like_number($folder) && $folder >= 1 && $folder <= 99) { + if ($folder <=15 && $track >= 1 && $track <= 3000) { + DFPlayerMini_AddToPlayQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_SetPlaybackFolder3000, ($folder << 4) | ($track >> 8), $track & 0xff), 0); + } else { + DFPlayerMini_AddToPlayQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_SetPlaybackFolder, $folder, $track), 0); + } + } else { + return "invalid folder $folder"; + } + } else { + Log3 $name, 5, "$name: track not found $arg"; + if ($arg =~ /^MD5/) { + readingsSingleUpdate($hash, "Missing_$arg", $hash->{LAST_TTS}, 0) if AttrVal($name, "rememberMissingTTS", 0); + return "no matching file found"; + } else { + return "can't find track $arg"; + } + } + } +} + +##################################### +sub +DFPlayerMini_AddToPlayQueue($$$) +{ + my ($hash, $msg, $isAdvert) = @_; + + if (@{$hash->{PLAYQUEUE}} == 0) { + # queue is empty, play immediately + DFPlayerMini_AddSendQueue($hash,$msg); + } + if ($isAdvert) { + # insert before the first non advert is there is one, else just add it at the end + for (my $i = 0; $i < @{$hash->{PLAYQUEUE}}; $i++) { + if (substr(@{$hash->{PLAYQUEUE}}[$i], 3, 1) != DFP_C_IntercutAdvert) { + splice @{$hash->{PLAYQUEUE}}, $i, 0, ($msg); + return; + } + } + } + push @{$hash->{PLAYQUEUE}}, $msg; + +} + +##################################### +sub +DFPlayerMini_Set($$$) +{ + my ($hash, $a, $h) = @_; + + return "\"set $hash->{NAME}\" needs at least one parameter" if(int(@$a) < 2); + if (!defined($sets{@$a[1]})) { + my $arguments = ' '; + foreach my $arg (sort keys %sets) { + $arguments.= $arg . ($sets{$arg} ? (':' . $sets{$arg}) : '') . ' '; + } + #Log3 $hash, 3, "$name: set arg = $arguments"; + return "Unknown argument @$a[1], choose one of " . $arguments; + } + + my $name = shift @$a; + my $cmd = shift @$a; + my $arg = join(" ", @$a); + my $ret = ""; + + if( $cmd eq "next" ) { + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_Next)); + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, ReadingsVal($name,"storage","SD") eq "SD" ? DFP_C_GetCurrentTrackSD : DFP_C_GetCurrentTrackUSB)); + } elsif( $cmd eq "prev" ) { + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_Previous)); + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, ReadingsVal($name,"storage","SD") eq "SD" ? DFP_C_GetCurrentTrackSD : DFP_C_GetCurrentTrackUSB)); + } elsif( $cmd eq "trackNum" ) { + return "track number must be between 1 and 3000" if (!looks_like_number($arg) || $arg < 1 || $arg > 3000); + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_TrackNum, $arg >> 8, $arg & 0xff)); + } elsif( $cmd eq "volumeUp" ) { + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_IncreaseVolume)); + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_GetVolume)); + } elsif( $cmd eq "volumeDown" ) { + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_DecreaseVolume)); + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_GetVolume)); + } elsif( $cmd eq "volumeStraight" ) { + return "volume must be between 0 and 30" if (!looks_like_number($arg) || $arg < 0 || $arg > 30); + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_SetVolume,0,$arg)); + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_GetVolume)); + } elsif( $cmd eq "equalizer" ) { + my $k; my $key; + my $val; + foreach $key (keys %equalizerTexts) { + if ($equalizerTexts{$key} eq $arg) { + $val = $equalizerTexts{$key}; + $k = $key; + last; + } + } + return "unknown input $arg" if !defined $val; + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_SetEqualizerMode, 0, $k)); + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_GetEqualizerMode)); + } elsif( $cmd eq "repeatSingle" ) { + return "track number must be between 1 and 99" if (!looks_like_number($arg) || $arg < 1 || $arg > 99); + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_SetRepeatSingle, $arg >> 8, $arg & 0xff)); + } elsif( $cmd eq "storage" ) { + my $k; my $key; + my $val; + foreach $key (keys %statusStorageTexts) { + if ($statusStorageTexts{$key} eq $arg) { + $val = $statusStorageTexts{$key}; + $k = $key; + last; + } + } + return "unknown input $arg" if !defined $val; + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_SetStorage, 0, $k)); + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_GetStorage)); + } elsif( $cmd eq "sleep" ) { + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_Sleep)); + } elsif( $cmd eq "wake" ) { + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_Wake)); + } elsif( $cmd eq "reset" ) { + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_Reset)); + } elsif( $cmd eq "play" ) { + if ($arg eq "") { + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_Play)); + } else { + $ret = DFPlayerMini_Play($hash, @$a); + } + if (!$ret) { + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, ReadingsVal($name,"storage","SD") eq "SD" ? DFP_C_GetCurrentTrackSD : DFP_C_GetCurrentTrackUSB)); + } + return $ret; + } elsif( $cmd eq "pause" ) { + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_Pause)); + readingsSingleUpdate($hash, "advertPossible", 0, 1); + } elsif( $cmd eq "amplification" ) { + return "amplification must be between 0 and 31" if (!looks_like_number($arg) || $arg < 0 || $arg > 31); + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_Amplification, $arg > 0 ? 1 : 0, $arg)); + } elsif( $cmd eq "repeatRoot" ) { + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_RepeatAllRoot, 0, $arg eq "on" ? 1 : 0)); + } elsif( $cmd eq "MP3TrackNum" ) { + return "track number must be between 1 and 65536" if (!looks_like_number($arg) || $arg < 1 || $arg > 65536); + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_SetPlaybackFolderMP3, $arg >> 8, $arg & 0xff)); + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, ReadingsVal($name,"storage","SD") eq "SD" ? DFP_C_GetCurrentTrackSD : DFP_C_GetCurrentTrackUSB)); + } elsif ($cmd eq "intercutAdvert") { + return "track number must be between 1 and 3000" if (!looks_like_number($arg) || $arg < 1 || $arg > 3000); + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_IntercutAdvert, $arg >> 8, $arg & 0xff)); + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, ReadingsVal($name,"storage","SD") eq "SD" ? DFP_C_GetCurrentTrackSD : DFP_C_GetCurrentTrackUSB)); + } elsif( $cmd eq "folderTrackNum" ) { + my ($folder, $track) = split(" ", $arg, 2); + return "folder and track must be numeric" if (!looks_like_number($folder) || !looks_like_number($track)); + if ($folder >= 1 && $folder <= 99 && $track >= 1 && $track <= 255) { + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_SetPlaybackFolder, $folder, $track)); + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, ReadingsVal($name,"storage","SD") eq "SD" ? DFP_C_GetCurrentTrackSD : DFP_C_GetCurrentTrackUSB)); + } else { + return "track or folder number out of range"; + } + } elsif( $cmd eq "folderTrackNum3000" ) { + my ($folder, $track) = split(" ", $arg, 2); + return "folder and track must be numeric" if (!looks_like_number($folder) || !looks_like_number($track)); + if ($folder >= 1 && $folder <=15 && $track >= 1 && $track <= 3000) { + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_SetPlaybackFolder3000, ($folder << 4) | ($track >> 8), $track & 0xff)); + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, ReadingsVal($name,"storage","SD") eq "SD" ? DFP_C_GetCurrentTrackSD : DFP_C_GetCurrentTrackUSB)); + } else { + return "track or folder number out of range"; + } + } elsif( $cmd eq "stopAdvert" ) { + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_StopAdvert)); + } elsif( $cmd eq "stop" ) { + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_Stop)); + readingsSingleUpdate($hash, "advertPossible", 0, 1); + } elsif( $cmd eq "repeatFolder" ) { + return "folder number must be between 1 and 99" if (!looks_like_number($arg) || $arg < 1 || $arg > 99); + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_RepeatFolder, 0, $arg)); + } elsif( $cmd eq "shuffle" ) { + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_Shuffle)); + } elsif( $cmd eq "repeatCurrentTrack" ) { + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_RepeatCurrent, 0, $arg eq "on" ? 1 : 0)); + } elsif( $cmd eq "DAC" ) { + DFPlayerMini_AddSendQueue($hash,DFPlayerMini_createCmd($hash, DFP_C_SetDAC, 0, $arg eq "on" ? 1 : 0)); + + } elsif( $cmd eq "close" ) { + $hash->{DevState} = 'closed'; + return DFPlayerMini_CloseDevice($hash); + } elsif($cmd eq "raw") { + Log3 $name, 4, "$name: set $name $cmd $arg"; + DFPlayerMini_AddSendQueue($hash,pack("H*", uc($arg))); + } elsif ($cmd eq "reopen") { + return DFPlayerMini_ResetDevice($hash); + } elsif ($cmd eq "readFiles") { + return DFPlayerMini_readFiles($hash); + } elsif ($cmd eq "uploadTTS") { + my $destFile = shift @$a; + my $ttsSay = join(" ", @$a); + return DFPlayerMini_uploadTTS($hash, $destFile, $ttsSay); + } elsif ($cmd eq "uploadNumbers") { + return DFPlayerMini_uploadNumbers($hash, $arg); + } elsif ($cmd eq "uploadTTScache") { + return DFPlayerMini_uploadTTScache($hash); + } elsif ($cmd eq "sayNumber") { + return DFPlayerMini_sayNumber($hash, $arg); + } elsif ($cmd eq "tts") { + my $ttsSay = join(" ", @$a); + return DFPlayerMini_tts($hash, $ttsSay); + } elsif ($cmd eq "response") { + return "response must be 10 hex bytes long" if length($arg) != 20; + DFPlayerMini_Parse($hash, pack("H*", substr($arg,6,8))); + #} elsif ($cmd eq "playlist") { + # DFPlayerMini_SimpleWrite($hash, pack("H*", "7EFF1521010201030104010501060201030504070509EF")); + } else { + Log3 $name, 5, "$name/set: set $name $cmd $arg"; + #DFPlayerMini_SimpleWrite($hash, $arg); + return "Unknown argument $cmd, choose one of ". ReadingsVal($name,'cmd',' help me'); + } + + return undef; +} + +##################################### +sub +DFPlayerMini_Get($$$) +{ + my ($hash, $a, $h) = @_; + my $type = $hash->{TYPE}; + my $name = $hash->{NAME}; + + Log3 $name, 5, "$name: \"get $type\" needs at least one parameter" if(@$a < 2); + return "\"get $name\" needs at least one parameter" if(@$a < 2); + if(!defined($gets{@$a[1]})) { + my $arguments = ' '; + foreach my $arg (sort keys %gets) { + $arguments.= $arg . ($gets{$arg}[1] ? (':' . $gets{$arg}[1]) : '') . ' '; + } + #Log3 $hash, 5, "$name: get arg = $arguments"; + + return "Unknown argument @$a[1], choose one of " . $arguments; + } + + my $cmd = @$a[1]; + my $arg = @$a[2]; + + #Log3 $name, 5, "$name: command for gets: $cmd" . unpack("H*", $gets{$cmd}[0]); + + + DFPlayerMini_AddSendQueue($hash, DFPlayerMini_createCmd($hash, $gets{$cmd}[0], undef, $arg)); + + return undef; +} + +##################################### +sub +DFPlayerMini_ResetDevice($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + Log3 $hash, 3, "$name reset"; + DevIo_CloseDev($hash); + my $ret = DevIo_OpenDev($hash, 0, "DFPlayerMini_DoInit", 'DFPlayerMini_Connect'); + + return $ret; +} + +##################################### +sub +DFPlayerMini_CloseDevice($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + Log3 $hash, 2, "$name closed"; + RemoveInternalTimer($hash); + DevIo_CloseDev($hash); + readingsSingleUpdate($hash, "state", "closed", 1); + + return undef; +} + +##################################### +sub +DFPlayerMini_DoInit($) +{ + my $hash = shift; + my $name = $hash->{NAME}; + my $err; + my $msg = undef; + + my ($ver, $try) = ("", 0); + #Dirty hack to allow initialisation of DirectIO Device for some debugging and tesing + Log3 $hash, 1, "$name/define: ".$hash->{DEF}; + + delete($hash->{disConnFlag}) if defined($hash->{disConnFlag}); + + RemoveInternalTimer("HandleWriteQueue:$name"); + @{$hash->{QUEUE}} = (); + if (($hash->{DEF} !~ m/\@DirectIO/) and ($hash->{DEF} !~ m/none/) ) + { + Log3 $hash, 1, "$name/init: ".$hash->{DEF}; + $hash->{initretry} = 0; + RemoveInternalTimer($hash); + + InternalTimer(gettimeofday() + DFP_INIT_WAIT, "DFPlayerMini_StartInit", $hash, 0); + } + # Reset the counter + delete($hash->{XMIT_TIME}); + delete($hash->{NR_CMD_LAST_H}); + + @{$hash->{PLAYQUEUE}} = (); + @{$hash->{TTSQUEUE}} = (); + + return; + #return undef; +} + +sub DFPlayerMini_StartInit($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + $hash->{storage} = undef; + + Log3 $name,3 , "$name/init: get storage, retry = " . $hash->{initretry}; + if ($hash->{initretry} >= DFP_INIT_MAXRETRY) { + $hash->{DevState} = 'INACTIVE'; + # einmaliger reset, wenn danach immer noch 'init retry count reached', dann DFPlayerMini_CloseDevice() + if (!defined($hash->{initResetFlag})) { + Log3 $name,2 , "$name/init retry count reached. Reset"; + $hash->{initResetFlag} = 1; + DFPlayerMini_ResetDevice($hash); + } else { + Log3 $name,2 , "$name/init retry count reached. Closed"; + DFPlayerMini_CloseDevice($hash); + } + return; + } + else { + DFPlayerMini_SimpleWrite($hash, DFPlayerMini_createCmd($hash, DFP_C_GetStorage)); + $hash->{DevState} = 'waitInit'; + RemoveInternalTimer($hash); + InternalTimer(gettimeofday() + DFP_CMD_TIMEOUT, "DFPlayerMini_CheckCmdResp", $hash, 0); + } +} + + +#################### +sub DFPlayerMini_CheckCmdResp($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + my $msg = undef; + my $storage; + + $storage = ReadingsVal($name, "storage", ""); + if ($storage) { + readingsSingleUpdate($hash, "state", "opened", 1); + Log3 $name, 2, "$name: initialized"; + $hash->{DevState} = 'initialized'; + delete($hash->{initResetFlag}) if defined($hash->{initResetFlag}); + delete($hash->{initretry}); + # initialize keepalive + $hash->{keepalive}{ok} = 0; + $hash->{keepalive}{retry} = 0; + my $keepAliveInterval = AttrVal($name, "keepAliveInterval", DFP_KEEPALIVE_TIMEOUT); + if ($keepAliveInterval > 0) { + InternalTimer(gettimeofday() + $keepAliveInterval, "DFPlayerMini_KeepAlive", $hash, 0); + } + } + else { + $hash->{initretry} ++; + DFPlayerMini_StartInit($hash); + } +} + + + +##################################### +## API to logical modules: Provide as Hash of IO Device, type of function ; command to call ; message to send +sub +DFPlayerMini_Write($$$) +{ + my ($hash,$fn,$msg) = @_; + my $name = $hash->{NAME}; + + $fn="RAW" if $fn eq ""; + + Log3 $name, 5, "$name/write: adding to queue $fn $msg"; + + #DFPlayerMini_SimpleWrite($hash, $bstring); + + #DFPlayerMini_Set($hash,$name,$fn,$msg); + #DFPlayerMini_AddSendQueue($hash,$bstring); + +} + + +sub DFPlayerMini_AddSendQueue($$) +{ + my ($hash, $msg) = @_; + my $name = $hash->{NAME}; + + #Log3 $hash, 3,"$name: AddSendQueue: " . $hash->{NAME} . ": $msg"; + + if (gettimeofday() - $hash->{LAST_SEND_TS} > DFP_MIN_WAITTIME) { + # minimal wait time before next command exceeded, can write immediately + DFPlayerMini_SimpleWrite($hash, $msg); + } else { + # have to wait before sending the next command + push(@{$hash->{QUEUE}}, $msg); + + #Log3 $hash , 5, Dumper($hash->{QUEUE}); + + InternalTimer(gettimeofday() + DFP_MIN_WAITTIME, "DFPlayerMini_HandleWriteQueue", "HandleWriteQueue:$name", 1); + } +} + + +sub +DFPlayerMini_SendFromQueue($$) +{ + my ($hash, $msg) = @_; + my $name = $hash->{NAME}; + + if($msg ne "") { + #DevIo_SimpleWrite($hash, $msg,2); + + DFPlayerMini_SimpleWrite($hash,$msg); + + } + + ############## + if (AttrVal($name, "requestAck", 0) == 0) { + # Write the next buffer not earlier than 20ms, which is the fastest time the DFP can process commands + InternalTimer(gettimeofday() + DFP_MIN_WAITTIME, "DFPlayerMini_HandleWriteQueue", "HandleWriteQueue:$name", 1); + } +} + +#################################### +sub +DFPlayerMini_HandleWriteQueue($) +{ + my($param) = @_; + my(undef,$name) = split(':', $param); + my $hash = $defs{$name}; + + #my @arr = @{$hash->{QUEUE}}; + + if(@{$hash->{QUEUE}}) { + my $msg= shift(@{$hash->{QUEUE}}); + + if($msg eq "") { + DFPlayerMini_HandleWriteQueue("x:$name"); + } else { + DFPlayerMini_SendFromQueue($hash, $msg); + } + } else { + Log3 $name, 4, "$name/HandleWriteQueue: nothing to send, stopping timer"; + RemoveInternalTimer("HandleWriteQueue:$name"); + } +} + +##################################### +# called from the global loop, when the select for hash->{FD} reports data +sub +DFPlayerMini_Read($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + # einlesen der bereitstehenden Daten + my $buf = DevIo_SimpleRead($hash); + return "" if ( !defined($buf) ); + + # Zum debuggen in lesbarer Form + $hash->{PARTIAL} = unpack ('H*', $buf); + + #Log3 $name, 5, "$name: DFPlayerMini_Read ($name) - received data: " . $hash->{PARTIAL}; + + # Daten an den Puffer anhängen + $hash->{helper}{BUFFER} .= $buf; + #Log3 $name, 5, "$name: DFPlayerMini_Read ($name) - current buffer content: " . unpack('H*', $hash->{helper}{BUFFER}); + + # prüfen, ob im Buffer ein vollständiger Frame mit 10 Bytes zur Verarbeitung vorhanden ist. + while (length($hash->{helper}{BUFFER}) >= DFP_FrameLength) + { + if (unpack("C",substr($hash->{helper}{BUFFER},0,1)) == DFP_Start_Byte && unpack("C", substr($hash->{helper}{BUFFER},9,1)) == DFP_End_Byte) { + # Checksumme prüfen + my $checksum = 0; + for (my $i=1; $i<=6; $i++) { + $checksum += unpack("C", substr($hash->{helper}{BUFFER},$i,1)); + } + $checksum = (0xffff - ($checksum) + 1) & 0xffff; + if ($checksum != unpack("n", substr($hash->{helper}{BUFFER},7,2))) { + Log3 $name, 2, "$name: DFPlayerMini_Read - invalid checksum: calc $checksum, received " . unpack("n", substr($hash->{helper}{BUFFER},7,2)); + } else { + DFPlayerMini_Parse($hash, substr($hash->{helper}{BUFFER},3,4)); + } + # remove processed command from buffer + $hash->{helper}{BUFFER} = substr($hash->{helper}{BUFFER},DFP_FrameLength); + } else { + Log3 $name, 2, "$name: DFPlayerMini_Read - no valid start/end byte"; + $hash->{helper}{BUFFER} = substr($hash->{helper}{BUFFER},1); + } + } +} + + + +sub DFPlayerMini_KeepAlive($){ + my ($hash) = @_; + my $name = $hash->{NAME}; + + return if ($hash->{DevState} eq 'disconnected'); + + Log3 $name,4 , "$name/KeepAliveOk: " . $hash->{keepalive}{ok}; + if (!$hash->{keepalive}{ok}) { + if ($hash->{keepalive}{retry} >= DFP_KEEPALIVE_MAXRETRY) { + Log3 $name,4 , "$name/keepalive retry count reached. Reset"; + $hash->{DevState} = 'INACTIVE'; + DFPlayerMini_ResetDevice($hash); + return; + } + elsif (@{$hash->{QUEUE}} == 0) { + $hash->{keepalive}{retry} ++; + Log3 $name,4 , "$name/KeepAlive: send requestAck"; + DFPlayerMini_AddSendQueue($hash, DFPlayerMini_createCmd($hash, DFP_C_Acknowledge)); + } + } + Log3 $name,4 , "$name/keepalive retry = " . $hash->{keepalive}{retry}; + $hash->{keepalive}{ok} = 0; + + my $keepAliveInterval = AttrVal($name, "keepAliveInterval", DFP_KEEPALIVE_TIMEOUT); + if ($keepAliveInterval > 0) { + InternalTimer(gettimeofday() + $keepAliveInterval, "DFPlayerMini_KeepAlive", $hash, 1); + } +} + + +### Helper Subs >>> + + + +sub +DFPlayerMini_Parse($$) +{ + my ($hash, $rmsg) = @_; + my $name = $hash->{NAME}; + my $error = ""; + my @storage; + my $state = ""; + + if (defined($hash->{keepalive})) { + $hash->{keepalive}{ok} = 1; + $hash->{keepalive}{retry} = 0; + } + + my $debug = AttrVal($hash->{NAME},"debug",0); + + + #Debug "$name: incoming message: ($rmsg)\n" if ($debug); + Log3 $name, 5, "$name: incoming message: (" .unpack("H*", $rmsg) . ")"; + + my ($cmd, $ack, $par1, $par2) = unpack("CCCC", $rmsg); + my $par16 = $par1 * 256 + $par2; + if ($cmd == DFP_C_Error) { + $error = $errorTexts{$par2}; + $error = "unknown Error" if !defined $error; + # ToDo: Special case: DFP_C_GetNoTracksFolder will return DFP_E_TrackNotFound if the folder is empty + readingsSingleUpdate($hash, "state", $error, 1); + } elsif ($cmd == DFP_C_Acknowledge) { + if (AttrVal($name, "requestAck", 0)) { + # send next command + DFPlayerMini_HandleWriteQueue("x:$name"); + } + } elsif ($cmd == DFP_C_GetStorage) { + if ($par2 & 0x01) { + push @storage, "USB"; + } + if ($par2 & 0x02) { + push @storage, "SD"; + } + if ($par2 & 0x04) { + push @storage, "PC"; + } + readingsSingleUpdate($hash, "storage", join(",",@storage), 1); + } elsif ($cmd == DFP_C_GetStatus) { + $state = $statusStorageTexts{$par1} . ", " . $statusModeTexts{$par2}; + readingsSingleUpdate($hash, "state", $state, 1); + } elsif ($cmd == DFP_C_GetNoTracksInFolder) { + readingsSingleUpdate($hash, "tracksInFolder_".$par2, $par1, 1); + } elsif ($cmd == DFP_C_GetNoTracksRootSD) { + readingsSingleUpdate($hash, "tracksRootSD", $par16, 1); + } elsif ($cmd == DFP_C_GetNoTracksRootUSB) { + readingsSingleUpdate($hash, "tracksRootUSB", $par16, 1); + } elsif ($cmd == DFP_C_GetNoFolders) { + readingsSingleUpdate($hash, "noFolders", $par16, 1); + } elsif ($cmd == DFP_C_GetVolume) { + readingsSingleUpdate($hash, "volumeStraight", $par2, 1); + } elsif ($cmd == DFP_C_GetEqualizerMode) { + readingsSingleUpdate($hash, "equalizer", $equalizerTexts{$par2}, 1); + } elsif ($cmd == DFP_C_SetPlaybackFolder3000 || $cmd == DFP_C_SetPlaybackFolder) { + + } elsif ($cmd == DFP_C_TrackFinishedSD || $cmd == DFP_C_TrackFinishedUSB) { + # there is a bug in the DFP, it sends this response twice in succession! + # and only after the second response it will accept the next play command. + # Especially if connected via WLAN the time between the two responses can be quite high + # -> only start the next play when the second response has been received + if ($hash->{helper}{LAST_RESPONSE} eq $rmsg) { # || gettimeofday() - $hash->{LAST_RECV_TS} > 10*DFP_MIN_WAITTIME) { + shift @{$hash->{PLAYQUEUE}}; + if (@{$hash->{PLAYQUEUE}} != 0) { + + # play the next track from queue + DFPlayerMini_AddSendQueue($hash, ${$hash->{PLAYQUEUE}}[0]); + } + readingsSingleUpdate($hash, "state", "track $par16 finished", 1); + } + } elsif ($cmd == DFP_C_GetCurrentTrackSD || $cmd == DFP_C_GetCurrentTrackUSB) { + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, "advertPossible", 1); + readingsBulkUpdate($hash, "currentTrack" . ($cmd == DFP_C_GetCurrentTrackSD ? "SD" : "USB"), $par16); + readingsBulkUpdate($hash, "state", "playing track $par16"); + readingsEndUpdate($hash, 1); + } elsif ($cmd == DFP_C_StoragePluggedIn) { + readingsSingleUpdate($hash, "state", "storage plugged in", 1); + } elsif ($cmd == DFP_C_StoragePulledOut) { + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, "advertPossible", 0); + readingsBulkUpdate($hash, "state", "storage pulled out", 1); + readingsEndUpdate($hash, 1); + @{$hash->{PLAYQUEUE}} = (); + } elsif ($cmd == DFP_C_RepeatCurrent) { + readingsSingleUpdate($hash, "repeat", $par2 == 1 ? "current" : "off", 1); + } elsif ($cmd == DFP_C_RepeatFolder) { + readingsSingleUpdate($hash, "repeat", $par2 == 1 ? "folder" : "off", 1); + } elsif ($cmd == DFP_C_RepeatAllRoot) { + readingsSingleUpdate($hash, "repeat", $par2 == 1 ? "root" : "off", 1); + } else { + Log3 $hash, 1, "$name: Unknown response " . sprintf("%02x %02x %02x", $cmd, $par1, $par2); + } + $hash->{helper}{LAST_RESPONSE} = $rmsg; + $hash->{LAST_RECV_TS} = gettimeofday(); +} + + +##################################### +sub +DFPlayerMini_Ready($) +{ + my ($hash) = @_; + + if ($hash->{STATE} eq 'disconnected') { + $hash->{DevState} = 'disconnected'; + return DevIo_OpenDev($hash, 1, "DFPlayerMini_DoInit", 'DFPlayerMini_Connect') + } + + # This is relevant for windows/USB only + my $po = $hash->{USBDev}; + my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags); + if($po) { + ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags) = $po->status; + } + return ($InBytes && $InBytes>0); +} + +######################## +sub +DFPlayerMini_SimpleWrite(@) +{ + my ($hash, $msg, $nonl) = @_; + return if(!$hash); + + my $name = $hash->{NAME}; + my $hexMsg = unpack ('H*', $msg); + Log3 $name, 5, "$name SW: $hexMsg"; + + my $sendCmd = AttrVal($name, "sendCmd", undef); + if (defined $sendCmd) { + $sendCmd =~ s/\$msg/${hexMsg}/; + Log3 $name, 5, "$name: sendCmd: $sendCmd"; + my $errors = AnalyzeCommandChain($hash, $sendCmd); + Log3 $name, 1, "$name: $errors" if $errors; + } else { + + $hash->{USBDev}->write($msg) if($hash->{USBDev}); + syswrite($hash->{TCPDev}, $msg) if($hash->{TCPDev}); + syswrite($hash->{DIODev}, $msg) if($hash->{DIODev}); + + # Some linux installations are broken with 0.001, T01 returns no answer + select(undef, undef, undef, 0.01); + } + + # remember time the command was sent + $hash->{LAST_SEND_TS} = gettimeofday(); + +} + +sub +DFPlayerMini_Attr(@) +{ + my ($cmd,$name,$aName,$aVal) = @_; + my $hash = $defs{$name}; + my $debug = AttrVal($name,"debug",0); + + if ($aName eq "TTSDev") + { +# if ($init_done) { +# my $devspec = "TYPE=Text2Speech:FILTER=NAME=$aVal"; +# my ($ttsDev) = devspec2array($devspec, $hash); +# if (!$ttsDev || !$defs{$ttsDev}) { +# return "$aVal does not exist or isn't of TYPE Text2Speech"; +# } + # we want to be notified when creating a tts file is finished + $hash->{NOTIFYDEV} = "global,$aVal"; + } + + return undef; +} + +sub DFPlayerMini_Notify($$) +{ + my ($own_hash, $dev_hash) = @_; + my $ownName = $own_hash->{NAME}; # own name / hash + + return "" if(IsDisabled($ownName)); # Return without any further action if the module is disabled + + my $devName = $dev_hash->{NAME}; # Device that created the events + + Log3 $ownName, 5, "Notify of $ownName called by event of $devName"; + + my $events = deviceEvents($dev_hash,1); + return if( !$events ); + + my $ttsDev = AttrVal($ownName, "TTSDev", "?"); + foreach my $event (@{$events}) { + $event = "" if(!defined($event)); + + # Examples: + # $event = "readingname: value" + # or + # $event = "INITIALIZED" (for $devName equal "global") + # + # processing $event with further code + if ($devName eq $ttsDev && defined $own_hash->{TTSQUEUE}) { + if (@{$own_hash->{TTSQUEUE}}) { + #event was triggered by receiving device + if ($event eq "playing: 0") { + + my $ttsCmd = shift @{$own_hash->{TTSQUEUE}}; + my ($destFile, $ttsSay) = split(/ /, $ttsCmd, 2); + Log3 $ownName, 4, "$ownName: tts finished with ttsCmd = $ttsCmd, destFile = $destFile, ttsSay = $ttsSay"; + + my $lastFilename = ReadingsVal($ttsDev, "lastFilename", ""); + if ($lastFilename) { + if ($destFile eq "!") { + # try to play using MD5 + my ($cachedir, $md5) = split(/\//, $lastFilename); + $md5 =~ s/\.mp3$//; + my $ret = DFPlayerMini_Play($own_hash, "MD5$md5"); + Log3 $ownName, 1, "$ownName: $ret" if $ret; + } else { + # upload file + $destFile = catfile(AttrVal($ownName, "uploadPath", ""), $destFile) . ".mp3"; + Log3 $ownName, 5, "$ownName: copying $lastFilename to $destFile"; + if (copy($lastFilename, $destFile) == 0) { + Log3 $ownName, 1, "$ownName: copying failed: $?"; + } + if (@{$own_hash->{TTSQUEUE}} == 0) { + Log3 $ownName, 4, "$ownName: tts, all playing done"; + $own_hash->{TTS_BUSY} = 0; + DFPlayerMini_readFiles($own_hash); + } else { + $ttsCmd = @{$own_hash->{TTSQUEUE}}[0]; + ($destFile, $ttsSay) = split(/ /, $ttsCmd, 2); + Log3 $ownName, 4, "$ownName: starting next tts $destFile $ttsSay"; + fhem "set $ttsDev tts " . $ttsSay; + } + } + } else { + Log3 $ownName, 1, "$ownName: $ttsDev has no lastFilename Reading, 98_Text2Speech module too old?"; + } + } + delete $own_hash->{DESTFILE}; + } + } elsif ($devName eq "global") { + if ($event eq "DELETEATTR $ownName TTSDev") { + delete $own_hash->{NOTIFYDEV}; + } + } + + } + return undef; +} + +1; + +=pod +=item device +=item summary supports the DFPLayer Mini FN-M16P Embedded MP3 Audio Module +=item summary_DE Unterstützt das DFPLayer Mini FN-M16P Embedded MP3 Audio Module +=begin html + + +

DFPlayerMini - FN-M16P Embedded MP3 Audio Module

+ + This module integrates the DFPlayerMini - FN-M16P Embedded MP3 Audio Module device into fhem. + See the datasheet of the module for technical details. +
+ The MP3 player can be connected directly to a serial interface or via ethernet/WiFi by using a hardware with offers a transparent + serial bridge over TCP/IP like ESPEasy Ser2Net. +

+ It is also possible to use other fhem transport devices like MYSENSORS. +

+ The module supports all commands of the DFPlayer and offers additional convenience functions like + +
+ + Define
+ define <name> DFPlayerMini {none | devicename[\@baudrate] | devicename\@directio | hostname:port} +
+ +
+ + + Attributes + + +
+ Get +

+ All query commands supported by the DFP have a corresponding get command: + + + + + + + + + + + + +
getDFP cmd byteparameterscomment
storage0x3F
status0x42
volume0x43
equalizer0x44
noTracksRootUsb0x47
noTracksRootSd0x48
currentTrackUsb0x4B
currentTrackSd0x4C
noTracksInFolder0x4Efolder number1-99
noFolders0x4F
+ +
+ Set +

+ All commands supported by the DFP have a corresponding set command: +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
setDFP cmd byteparameterscomment
next0x01-
prev0x02-
trackNum0x03number of track in root directorybetween 1 and 3000 (uses the order in which the files where created!)
volumeUp0x04-
volumeDown0x05-
volumeStraight0x06volume0-30
equalizer0x07name of the equalizer modeNormal, Pop, Rock, Jazz, Classic, Bass
repeatSingle0x08-
storage0x09SD or USB
sleep0x0A-not supported by DFP, DFP needs power cycle to work again
wake0x0B-not supported by DFP, but probably by FN-M22P
reset0x0C-
play0x0D-plays the current track
play0x0F, 0x12, 0x13, 0x14a space separated list of files to play successivelythe correct DFP command is used automatically. + Files can be specified with either their reading name, reading value or folder name/track number. + See set readFiles
pause0x0E-
amplification0x10level of amplification0-31
repeatRoot0x11on, off
MP3TrackNum0x12tracknumber1-3000, from folder MP3
intercutAdvert0x13tracknumber1-3000, from folder ADVERT
folderTrackNum0x0Ffoldernumber tracknumberfolder: 1-99, track: 1-255
folderTrackNum30000x14foldernumber tracknumberfolder: 1-15, track: 1-3000
stopAdvert0x15-
stop0x16-
repeatFolder0x17number of folder1-99
shuffle0x18-
repeatCurrentTrack0x19on, off
DAC0x1Aon, off
+
+ All other set commands are not sent to the DFP but offer convenience functions: +
+ + +=end html +=begin html_DE + + +

DFPlayerMini - FN-M16P Embedded MP3 Audio Module

+ Dieses Modul integriert den DFPlayerMini - FN-M16P Embedded MP3 Audio Modul in fhem. + Siehe auch das Datenblatt des Moduls für technische Details. +
+ Der MP3-Spieler kann direkt mit einer seriellen Schnittstelle verbunden werden oder per Ethernet/WiFi mittels einer Hardware die eine transparente + serielle Übertragung per TCP/IP zur Verfügung stellt, z. B. ESPEasy Ser2Net. +

+ Es ist auch möglich ein anderes fhem Device für den Datentransport zu nutzen, z. B. MYSENSORS. +

+ Das Modul unterstützt alle Kommandos des DFPlayers und bietet weitere Funktionen wie + +
+ + Define
+ define <name> DFPlayerMini {none | devicename[\@baudrate] | devicename\@directio | hostname:port} +
+ +
+ + + Attribute + + +
+ Get +

+ Alle Abfrage Kommandos die vom DFP unterstützt werden haben ein zugehöriges get Kommando: + + + + + + + + + + + + +
getDFP cmd byteParameterKommentar
storage0x3F
status0x42
volume0x43
equalizer0x44
noTracksRootUsb0x47
noTracksRootSd0x48
currentTrackUsb0x4B
currentTrackSd0x4C
noTracksInFolder0x4EVerzeichnisnummer1-99
noFolders0x4F
+ +
+ Set +

+ Alle Kommandos die vom DFP angeboten werden haben ein zugehöriges set Kommando: +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
setDFP cmd byteParameterKommentar
next0x01-
prev0x02-
trackNum0x03Nummer der Datei im Wurzelverzeichniszwischen 1 und 3000 (es wird die Reihenfolge verwendet in der die Dateien angelegt wurden!)
volumeUp0x04-
volumeDown0x05-
volumeStraight0x06Lautstärke0-30
equalizer0x07Name des EqualizermodusNormal, Pop, Rock, Jazz, Classic, Bass
repeatSingle0x08-
storage0x09SD oder USB
sleep0x0A-vom DFP nicht unterstützt, danach muss er stromlos gemacht werden um wieder zu funktionieren
wake0x0B-vom DFP nicht unterstützt, aber wahrscheinlich vom FN-M22P
reset0x0C-
play0x0D-spielt die aktuelle Datei
play0x0F, 0x12, 0x13, 0x14Eine durch Leerzeichen getrennte Liste von Dateien die nacheinander abgespielt werdenDas korrekte DFP Kommando wird automatisch ermittelt. + Dateien können über den Namen ihres Readings, den Readingwert oder Verzeichnisname/Dateinummer angegeben werden. Siehe set readFiles
pause0x0E-
amplification0x10Verstärkungsstufe0-31
repeatRoot0x11on, off
MP3TrackNum0x12Dateinummer1-3000, aus dem Verzeichnis MP3
intercutAdvert0x13Dateinummer1-3000, aus dem Verzeichnis ADVERT
folderTrackNum0x0FVerzeichnisnummer DateinummerVerzeichnis: 1-99, Datei: 1-255
folderTrackNum30000x14Verzeichnisnummer DateinummerVerzeichnis: 1-15, Datei: 1-3000
stopAdvert0x15-
stop0x16-
repeatFolder0x17Verzeichnisnummer1-99
shuffle0x18-
repeatCurrentTrack0x19on, off
DAC0x1Aon, off
+
+ Alle anderen set Kommandos werden nicht an den DFPlayer geschickt sondern bieten Komfortfunktionen: +
+ + +=end html_DE +=cut diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index 653b73078..4a832620c 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -17,6 +17,7 @@ configDB.pm betateilchen http://forum.fhem.de Sonstiges FHEM/00_CM11.pm neubert http://forum.fhem.de SlowRF FHEM/00_CUL.pm rudolfkoenig http://forum.fhem.de SlowRF +FHEM/00_DFPlayerMini.pm kaihs http://forum.fhem.de Multimedia FHEM/00_FBAHA.pm rudolfkoenig http://forum.fhem.de FRITZ!Box FHEM/00_FBAHAHTTP.pm rudolfkoenig http://forum.fhem.de FRITZ!Box FHEM/00_FHZ.pm rudolfkoenig http://forum.fhem.de SlowRF