02_FRAMEBUFFER.pm 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. #
  2. #
  3. # 02_FRAMEBUFFER.pm
  4. # written by Kai Stuke
  5. # based on 02_RSS.pm
  6. #
  7. ##############################################
  8. # $Id: 02_FRAMEBUFFER.pm 12126 2016-09-06 18:35:26Z kaihs $
  9. package main;
  10. use strict;
  11. use warnings;
  12. use GD;
  13. use feature qw/switch/;
  14. use vars qw(%data);
  15. use Scalar::Util qw(looks_like_number);
  16. require "02_RSS.pm"; # enable use of layout files and image creation
  17. my %sets = (
  18. 'updateDisplay' => "",
  19. 'relLayoutNo' => "",
  20. 'absLayoutNo' => "",
  21. 'layoutFilename' => "",
  22. );
  23. ##################################################
  24. # Forward declarations
  25. #
  26. ##################
  27. sub FRAMEBUFFER_Initialize($);
  28. sub FRAMEBUFFER_rewindCounter($);
  29. sub FRAMEBUFFER_readLayout($);
  30. sub FRAMEBUFFER_Define($$);
  31. sub FRAMEBUFFER_updateDisplay($$);
  32. sub FRAMEBUFFER_Set($@);
  33. sub FRAMEBUFFER_Attr(@);
  34. sub FRAMEBUFFER_returnPNG($);
  35. sub
  36. FRAMEBUFFER_Initialize($) {
  37. my ($hash) = @_;
  38. $hash->{DefFn} = "FRAMEBUFFER_Define";
  39. $hash->{AttrFn} = "FRAMEBUFFER_Attr";
  40. $hash->{AttrList} = 'loglevel:0,1,2,3,4,5,6 update_interval:1,2,5,10,20,30 ' .
  41. 'size layoutBasedir layoutList startLayoutNo debugFile bgcolor enableCmd disableCmd ' . $readingFnAttributes;
  42. $hash->{SetFn} = "FRAMEBUFFER_Set";
  43. $hash->{UndefFn} = 'FRAMEBUFFER_Undef';
  44. }
  45. sub FRAMEBUFFER_Undef($$) {
  46. my ($hash, $arg) = @_;
  47. RemoveInternalTimer($hash);
  48. return undef;
  49. }
  50. ##################
  51. sub FRAMEBUFFER_rewindCounter($) {
  52. my ($hash) = @_;
  53. my $name= $hash->{NAME};
  54. my $updateInterval = AttrVal($hash->{NAME}, 'update_interval', 0);
  55. Log3 $name, 5, "rewindCounter $updateInterval";
  56. if ($updateInterval > 0) {
  57. # round to the begin of the next minute to get a more accurate time display
  58. my $currentTime = time();
  59. my $triggerTime = int(($currentTime + ($updateInterval * 60))/60)*60;
  60. Log3 $name, 5, "current $currentTime next trigger at $triggerTime";
  61. InternalTimer($triggerTime, 'FRAMEBUFFER_rewindCounter', $hash, 0);
  62. }
  63. FRAMEBUFFER_updateDisplay($hash, 0);
  64. }
  65. ##################
  66. sub FRAMEBUFFER_readLayout($) {
  67. my ($hash) = @_;
  68. my $name= $hash->{NAME};
  69. my $filename= $hash->{fhem}{filename};
  70. if (!defined $filename) {
  71. return 0;
  72. }
  73. if (defined $hash->{layoutBasedir} && substr($filename,0,1) ne '/') {
  74. $filename = $hash->{layoutBasedir} . '/' . $filename;
  75. }
  76. if(open(LAYOUT, $filename)) {
  77. my @layout= <LAYOUT>;
  78. $hash->{fhem}{layout}= join("", @layout);
  79. close(LAYOUT);
  80. return 1;
  81. } else {
  82. $hash->{fhem}{layout}= ();
  83. Log3 $name, 1, "Cannot open $filename";
  84. return 0;
  85. }
  86. }
  87. ##################
  88. sub FRAMEBUFFER_Define($$) {
  89. my ($hash, $def) = @_;
  90. my @a = split("[ \t]+", $def);
  91. return "Usage: define <name> FRAMEBUFFER framebuffer_device" if(int(@a) != 3);
  92. my $name= $a[0];
  93. my $fb_device= $a[2];
  94. if (! (-r $fb_device && -w $fb_device)) {
  95. return "$fb_device isn't readable and writable";
  96. }
  97. $hash->{fhem}{fb_device}= $fb_device;
  98. eval "use GD::Text::Align";
  99. $hash->{fhem}{useTextAlign} = ($@ ? 0 : 1 );
  100. if(!($hash->{fhem}{useTextAlign})) {
  101. Log3 $hash, 2, "$name: Cannot use text alignment: $@";
  102. }
  103. eval "use GD::Text::Wrap";
  104. $hash->{fhem}{useTextWrap} = ($@ ? 0 : 1 );
  105. if(!($hash->{fhem}{useTextWrap})) {
  106. Log3 $hash, 2, "$name: Cannot use text wrapping: $@";
  107. }
  108. readingsSingleUpdate($hash, 'state', 'Initialized',1);
  109. return undef;
  110. }
  111. ##################
  112. sub FRAMEBUFFER_updateDisplay($$) {
  113. my ($hash, $timeout) = @_;
  114. my $name = $hash->{NAME};
  115. my $fbv = '/usr/local/bin/fbvs';
  116. my $fd = $hash->{fd};
  117. if (defined $fd) {
  118. close $fd;
  119. }
  120. if (-x $fbv) {
  121. if (defined $hash->{debugFile}) {
  122. use File::Spec;
  123. my $dfile = $hash->{debugFile};
  124. my($vol,$dir,$file) = File::Spec->splitpath($dfile);
  125. if ((-e $dfile && -w $dfile) || -w $dir) {
  126. $fbv = "tee $dfile | $fbv";
  127. }
  128. }
  129. # check if this is a display with a timeout
  130. if ($timeout) {
  131. # yes, execute enable command (e.g. enable backlight)
  132. fhem($hash->{enableCmd}) if $hash->{enableCmd};
  133. } else {
  134. # yes, execute enable command (e.g. enable backlight)
  135. fhem($hash->{disableCmd}) if $hash->{disableCmd};
  136. }
  137. if (FRAMEBUFFER_readLayout($hash)) {
  138. open($fd, "|".$fbv . ' -d '. $hash->{fhem}{fb_device});
  139. binmode $fd;
  140. print $fd FRAMEBUFFER_returnPNG($name);
  141. # don't close the file immediately, as this will wait
  142. # for the fbv process to terminate which may take some time
  143. #close FBV;
  144. }
  145. } else {
  146. Log3 $name, 1, "$fbv doesn't exist or isn't executable, please install it";
  147. }
  148. }
  149. ##################
  150. sub
  151. FRAMEBUFFER_Set($@) {
  152. my ($hash, @a) = @_;
  153. my $name =$a[0];
  154. my $cmd = $a[1];
  155. my $val = $a[2];
  156. my $val2 = $a[3];
  157. # usage check
  158. my $usage= "Unknown argument, choose one of " . join(' ', keys %sets);
  159. if (@a == 2) {
  160. if ($cmd eq "updateDisplay") {
  161. # just display the current layout again
  162. FRAMEBUFFER_updateDisplay($hash, 0);
  163. $usage = undef;
  164. }
  165. } elsif (@a == 3 || @a == 4) {
  166. if ($cmd eq "absLayoutNo") {
  167. my $layoutNo = (defined($val) && looks_like_number($val) && $val >= 0) ? $val : 0;
  168. my @layoutList = split(/ /,$hash->{layoutList});
  169. my $noOfLayouts = @layoutList;
  170. if ($val < $noOfLayouts) {
  171. $hash->{fhem}{filename} = $layoutList[$layoutNo];
  172. $hash->{fhem}{absLayoutNo} = $layoutNo;
  173. FRAMEBUFFER_updateDisplay($hash, 0);
  174. $usage = undef;
  175. } else {
  176. $usage = "absLayoutNo out of bounds, must be between 0 and $noOfLayouts";
  177. }
  178. } elsif ($cmd eq "relLayoutNo") {
  179. my $relLayoutNo = (defined($val) && looks_like_number($val)) ? $val : 0;
  180. my @layoutList = split(/ /,$hash->{layoutList});
  181. my $noOfLayouts = @layoutList;
  182. if ($noOfLayouts > 0) {
  183. $hash->{fhem}{absLayoutNo} += $relLayoutNo;
  184. if ($hash->{fhem}{absLayoutNo} > $noOfLayouts-1) {
  185. $hash->{fhem}{absLayoutNo} = $noOfLayouts-1;
  186. } elsif ($hash->{fhem}{absLayoutNo} < 0) {
  187. $hash->{fhem}{absLayoutNo} = 0;
  188. }
  189. $hash->{fhem}{filename} = $layoutList[$hash->{fhem}{absLayoutNo}];
  190. FRAMEBUFFER_updateDisplay($hash, 0);
  191. $usage = undef;
  192. } else {
  193. $usage = "layoutList is empty, please set that attribute first";
  194. }
  195. } elsif ($cmd eq "layoutFilename") {
  196. my $timeout = (defined($val2) && looks_like_number($val2) && $val2 >= 0) ? $val2 : 0;
  197. my $prevFilename = $hash->{fhem}{filename};
  198. $hash->{fhem}{filename} = $val;
  199. FRAMEBUFFER_updateDisplay($hash, $timeout);
  200. if ($timeout > 0) {
  201. # nach timeout Sekunden wieder das aktuelle Layout anzeigen
  202. RemoveInternalTimer($hash);
  203. $hash->{fhem}{filename} = $prevFilename;
  204. InternalTimer(time() + $timeout, 'FRAMEBUFFER_rewindCounter', $hash, 0);
  205. }
  206. $usage = undef;
  207. }
  208. readingsBeginUpdate($hash);
  209. readingsBulkUpdate($hash,"absLayoutNo", $hash->{fhem}{absLayoutNo});
  210. readingsBulkUpdate($hash,"layoutFilename", $hash->{fhem}{filename});
  211. readingsEndUpdate($hash,1);
  212. }
  213. return $usage;
  214. }
  215. ###################
  216. sub
  217. FRAMEBUFFER_Attr(@)
  218. {
  219. my (undef, $name, $attr, $val) = @_;
  220. my $hash = $defs{$name};
  221. my $msg = '';
  222. Log3 $name, 5, "attr " . $attr . " val " . $val;
  223. if ($attr eq 'debugFile') {
  224. $hash->{debugFile} = $val;
  225. } elsif ($attr eq 'update_interval') {
  226. my $updateInterval = (defined($val) && looks_like_number($val) && $val >= 0) ? $val : -1;
  227. if ($updateInterval >= 0) {
  228. if ($updateInterval != AttrVal($hash->{NAME}, 'update_interval', 0)) {
  229. RemoveInternalTimer($hash);
  230. $hash->{updateInterval} = $updateInterval;
  231. if ($val > 0) {
  232. InternalTimer(1, 'FRAMEBUFFER_rewindCounter', $hash, 0);
  233. }
  234. }
  235. } else {
  236. $msg = 'Wrong update_interval defined. update_interval must be a number >= 0';
  237. }
  238. } elsif ($attr eq 'layoutBasedir') {
  239. my $layoutBasedir = $val;
  240. if (-d $val && -r $val) {
  241. $hash->{layoutBasedir} = $val;
  242. } else {
  243. $msg = "$val is not a readable directory";
  244. }
  245. } elsif ($attr eq 'layoutList') {
  246. $hash->{layoutList} = $val;
  247. } elsif ($attr eq 'startLayoutNo') {
  248. # Beim start des Moduls das anzuzeigenden Layout aus diesem Attribut nehmen
  249. if (!defined $hash->{fhem}{absLayoutNo}) {
  250. fhem "set $name absLayoutNo $val" ;
  251. }
  252. } elsif ($attr eq 'bgcolor') {
  253. } elsif ($attr eq 'enableCmd') {
  254. $hash->{enableCmd} = $val;
  255. } elsif ($attr eq 'disableCmd') {
  256. $hash->{disableCmd} = $val;
  257. }
  258. return ($msg) ? $msg : undef;
  259. }
  260. ##################
  261. sub
  262. FRAMEBUFFER_returnPNG($) {
  263. my ($name)= @_;
  264. my ($width,$height)= split(/x/, AttrVal($name,"size","128x160"));
  265. #
  266. # increase counter
  267. #
  268. if(defined($defs{$name}{fhem}) && defined($defs{$name}{fhem}{counter})) {
  269. $defs{$name}{fhem}{counter}++;
  270. } else {
  271. $defs{$name}{fhem}{counter}= 1;
  272. }
  273. # true color
  274. GD::Image->trueColor(1);
  275. #
  276. # create the image
  277. #
  278. my $S;
  279. # let's create a blank image, we will need it in most cases.
  280. $S= GD::Image->newTrueColor($width,$height);
  281. my $bgcolor = AttrVal($name,'bgcolor','000000'); #default bg color = black
  282. $bgcolor = RSS_color($S, $bgcolor);
  283. # $S->colorAllocate(0,0,0); # other colors seem not to work (issue with GD)
  284. $S->fill(0,0,$bgcolor);
  285. # wrap to make problems with GD non-lethal
  286. #
  287. # evaluate layout
  288. #
  289. eval { RSS_evalLayout($S, $name, $defs{$name}{fhem}{layout}) };
  290. Log3 $name, 1, "Problem with layout " . $defs{$name}{fhem}{layout} . ", maybe wrong syntax or included images don't exist: $@" if $@ ne "";
  291. #
  292. # return png image
  293. #
  294. return $S->png(0);
  295. }
  296. 1;
  297. =pod
  298. =item device
  299. =item summary Graphical display on a Linux framebuffer
  300. =begin html
  301. <a name="FRAMEBUFFER"></a>
  302. <h3>FRAMEBUFFER</h3>
  303. <ul>
  304. Provides a device to display arbitrary content on a linux framebuffer device<p>
  305. You need to have the perl module <code>GD</code> installed. This module is most likely not
  306. available for small systems like Fritz!Box.<p>
  307. FRAMEBUFFER uses <a href="#RSS">RSS</a> to create an image that is displayed on the framebuffer.<br>
  308. The binary program fbvs is required to display the image. You can download it from <a href="https://github.com/kaihs/fbvs">github</a>.
  309. </p>
  310. <a name="FRAMEBUFFERdefine"></a>
  311. <b>Define</b>
  312. <ul>
  313. <code>define &lt;name&gt; FRAMEBUFFER &lt;framebuffer_device_name&gt;</code><br><br>
  314. Defines a framebuffer device. <code>&lt;framebuffer_device_name&gt;</code> is the name of the linux
  315. device file for the kernel framebuffer device, e.g. /dev/fb1 or /dev/fb0.
  316. Examples:
  317. <ul>
  318. <code>define display FRAMEBUFFER /dev/fb1</code><br>
  319. <code>define TV FRAMEBUFFER /dev/fb0</code><br>
  320. </ul>
  321. <br>
  322. </ul>
  323. <a name="FRAMBUFFERset"></a>
  324. <b>Set</b>
  325. <ul>
  326. <code>set &lt;name&gt; absLayoutNo &lt;number&gt;</code>
  327. <br><br>
  328. A list of layout files can be defined with <code>attr layoutList</code>, see below.
  329. This command selects the layout with the given number from this list and displays it.
  330. This can e.g. be useful if bound to a key of a remote control to display a specific layout.
  331. <br><br>
  332. </ul>
  333. <ul>
  334. <code>set &lt;name&gt; layoutFilename &lt;name&gt; [&lt;timeout in seconds&gt;]</code>
  335. <br><br>
  336. Displays the image described by the layout in file &lt;name&gt;. If &lt;name&gt; is an absolute path
  337. name it is used as is to access the file. Otherwise the attribute &lt;layoutBasedir&gt; is prepended to
  338. the &lt;name&gt;.
  339. If a timeout is given, the image is only displayed for timeout seconds before the previously displayed image
  340. is displayed again.
  341. Useful for displaying an image only for a certain time after an event has occured.
  342. <br><br>
  343. </ul>
  344. <ul>
  345. <code>set &lt;name&gt; relLayoutNo &lt;number&gt;</code>
  346. <br><br>
  347. Like absLayoutNo this displays a certain image from the layoutList. Here &lt;number&gt; is added to the current
  348. layout number.
  349. So<br>
  350. <code>set &lt;name&gt; relLayoutNo 1</code>
  351. displays the next image from the list while<br>
  352. <code>set &lt;name&gt; relLayoutNo -1</code><br>
  353. displays the previous one.
  354. Useful if bound to a next/previous key on a remote control to scroll through all defined layouts.
  355. <br><br>
  356. </ul>
  357. <ul>
  358. <code>set &lt;name&gt; updateDisplay</code>
  359. <br><br>
  360. Refreshes the display defined by the currently active layout.
  361. <br><br>
  362. </ul>
  363. <a name="FRAMEUFFERattr"></a>
  364. <b>Attributes</b>
  365. <br>
  366. <ul>
  367. <code>size &lt;width&gt;x&lt;height&gt;</code><br>
  368. The dimensions of the display in pixels.
  369. Images will generated using this size. If the size is greater than the actual display
  370. size they will be scaled to fit. As this requires computing performance it should be avoided by
  371. defining the size to match the display size.
  372. <br>Example<br>
  373. <code>attr &lt;name&gt; size 128x160</code>
  374. <br><br>
  375. </ul>
  376. <ul>
  377. <code>layoutBasedir &lt;directory name&gt;</code><br>
  378. Directory that contains the layout files. If a layout filename is specified using a relative path
  379. <code>layoutBasedir</code> will be prepended before accessing the file.
  380. <br>Example<br>
  381. <code>attr &lt;name&gt; layoutBasedir /opt/fhem/layouts</code>
  382. <br><br>
  383. </ul>
  384. <ul>
  385. <code>layoutList &lt;file1&gt; [&lt;file2&gt;] ...</code>
  386. <br>Space separated list of layout files.
  387. These will be used by <code>absLayoutNo</code> and <code>relLayoutNo</code>.
  388. <code>layoutBasedir</code> will be prepended to each file if it is a relative path.
  389. <br>Example<br>
  390. <code>attr &lt;name&gt; layoutList standard.txt wetter.txt schalter.txt</code>
  391. <br><br>
  392. </ul>
  393. <ul>
  394. <code>update_interval &lt;interval&gt;</code>
  395. <br>Update interval in minutes.
  396. The currently displayed layout will be refreshed every &lt;interval&gt; minutes. The first
  397. interval will be scheduled to the beginning of the next minute to help create an accurate
  398. time display.<br>
  399. <br>Example<br>
  400. <code>attr &lt;name&gt; update_interval 1</code>
  401. <br><br>
  402. </ul>
  403. <ul>
  404. <code>debugFile &lt;file&gt;</code><br>
  405. Normally the generated image isn't written to a file. To ease debugging of layouts the generated image is written to the
  406. filename specified by this attribute.
  407. This attribute shouldn't be set during normal operation.
  408. <br><br>
  409. </ul>
  410. <ul>
  411. <code>startLayoutNo &lt;number&gt;</code><br>
  412. The number of the layout to be displayed on startup of the FRAMEBUFFER device.
  413. <br><br>
  414. </ul>
  415. <ul>bgcolor &lt;color&gt;<br>Sets the background color. &lt;color&gt; is
  416. a 6-digit hex number, every 2 digits determining the red, green and blue
  417. color components as in HTML color codes (e.g.<code>FF0000</code> for red, <code>C0C0C0</code> for light gray).
  418. <br><br>
  419. </ul>
  420. <ul>enableCmd &lt;fhem cmd&gt;<br>
  421. if set this command is executed before a layout with a timeout is displayed. This can e.g. be used to enable a backlight.
  422. <br><br>
  423. </ul>
  424. <ul>disableCmd &lt;fhem cmd&gt;<br>
  425. if set this command is executed after a layout with a timeout has expired. This can e.g. be used to disable a backlight.
  426. </ul>
  427. <br><br>
  428. <b>Usage information</b>
  429. <br>
  430. <ul>
  431. This module requires the binary program fbvs to be installed in /usr/local/bin and it must be executable
  432. by user fhem.
  433. fbvs (framebuffer viewer simple) is a stripped down version of fbv that can only display png images. It reads
  434. the image from stdin, displays it on the framebuffer and terminates afterwards.
  435. This module generates a png image based on a layout description internally and then pipes it to fbvs for display.
  436. </ul>
  437. <br>
  438. <a name="FRAMEBUFFERlayout"></a>
  439. <b>Layout definition</b>
  440. <br>
  441. <ul>
  442. FRAMEBUFFER uses the same <a href="#RSSlayout">layout definition</a> as <a href="#RSS">RSS</a>. In fact FRAMEBUFFER calls RSS to generate an image.
  443. </ul>
  444. </ul>
  445. =end html
  446. =cut