############################################## # $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 { DFPlayerMini_DoInit($hash); $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}} = (); $hash->{waitForAck} = 0; 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 (!$hash->{waitForAck} && 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}); if ($hash->{waitForAck} == 0) { # if we don't have to wait for an acknowledge from dfp we have at least to wait 20ms InternalTimer(gettimeofday() + DFP_MIN_WAITTIME, "DFPlayerMini_HandleWriteQueue", "HandleWriteQueue:$name", 1); } else { Log3 $hash, 5, "delayed send, waiting for ack"; } } } #################################### 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_SimpleWrite($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}); # prufen, 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) { # send next command if one is in the queue $hash->{waitForAck} = 0; 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(); # evaluate requestAck byte in command # if it is set an acknowledge must be received before # sending the next command from the queue $hash->{waitForAck} = unpack('C', substr($msg,4,1)); Log3 $name, 5, "current cmd waitForAck " . $hash->{waitForAck}; } 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