leds.pl 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. #!/usr/bin/perl
  2. use strict;
  3. use warnings;
  4. use POSIX;
  5. use File::Touch;
  6. use Math::Random;
  7. use Time::HiRes qw( usleep );
  8. # Local modules
  9. use Cwd qw(abs_path);
  10. use File::Basename qw(dirname);
  11. use lib dirname(abs_path($0));
  12. use DMX;
  13. # Prototypes
  14. sub red_alert();
  15. sub rave();
  16. sub rave_loop();
  17. # Effects states
  18. my %EFFECTS = (
  19. 'RED_ALERT' => \&red_alert,
  20. 'RAVE' => \&rave,
  21. );
  22. # User config
  23. my $COLOR_TIMEOUT = 30;
  24. my $COLOR_TIME_MIN = int($COLOR_TIMEOUT / 2);
  25. my %COLOR_VAR = (
  26. 'PLAY' => 0.50,
  27. 'PLAY_HIGH' => 0.50,
  28. 'PAUSE' => 0.65,
  29. 'MOTION' => 0.15,
  30. );
  31. my %DIM = (
  32. 'OFF' => [
  33. { 'channel' => 13, 'value' => 0, 'time' => 60000 },
  34. { 'channel' => 14, 'value' => 0, 'time' => 60000 },
  35. { 'channel' => 15, 'value' => 0, 'time' => 60000 },
  36. ],
  37. 'PLAY' => [
  38. { 'channel' => 13, 'value' => 10, 'time' => 500 },
  39. { 'channel' => 14, 'value' => 10, 'time' => 500 },
  40. { 'channel' => 15, 'value' => 8, 'time' => 500 },
  41. ],
  42. 'PLAY_HIGH' => [
  43. { 'channel' => 13, 'value' => 64, 'time' => 1000, 'delay' => 3000 },
  44. { 'channel' => 14, 'value' => 64, 'time' => 1000, 'delay' => 1500 },
  45. { 'channel' => 15, 'value' => 64, 'time' => 1000, 'delay' => 0 },
  46. ],
  47. 'PAUSE' => [
  48. { 'channel' => 13, 'value' => 96, 'time' => 3000, 'delay' => 3000 },
  49. { 'channel' => 14, 'value' => 96, 'time' => 3000, 'delay' => 0 },
  50. { 'channel' => 15, 'value' => 96, 'time' => 3000, 'delay' => 7000 },
  51. ],
  52. 'MOTION' => [
  53. { 'channel' => 13, 'value' => 144, 'time' => 1000 },
  54. { 'channel' => 14, 'value' => 144, 'time' => 1000 },
  55. { 'channel' => 15, 'value' => 144, 'time' => 1000 },
  56. ],
  57. );
  58. # App config
  59. my $DATA_DIR = DMX::dataDir();
  60. my $OUTPUT_FILE = $DATA_DIR . 'LED';
  61. my $STATE_SOCK = $OUTPUT_FILE . '.socket';
  62. my $RAVE_FILE = $DATA_DIR . 'RAVE';
  63. my $PUSH_TIMEOUT = 20;
  64. my $PULL_TIMEOUT = $PUSH_TIMEOUT * 3;
  65. my $DELAY = $PULL_TIMEOUT / 2;
  66. # Debug
  67. my $DEBUG = 0;
  68. if ($ENV{'DEBUG'}) {
  69. $DEBUG = 1;
  70. }
  71. # Reset the timeouts if the color delay demands it
  72. if ($PUSH_TIMEOUT < $COLOR_TIMEOUT) {
  73. $PUSH_TIMEOUT = $COLOR_TIMEOUT / 2;
  74. }
  75. if ($DELAY > $COLOR_TIMEOUT / 2) {
  76. $DELAY = $COLOR_TIMEOUT / 2;
  77. }
  78. # Construct a list of valid states
  79. my %VALID = ();
  80. foreach my $key (keys(%DIM)) {
  81. $VALID{$key} = 1;
  82. }
  83. foreach my $key (keys(%EFFECTS)) {
  84. $VALID{$key} = 1;
  85. }
  86. $VALID{'STOP'} = 1;
  87. # Sockets
  88. DMX::stateSocket($STATE_SOCK);
  89. DMX::stateSubscribe($STATE_SOCK);
  90. # State
  91. my $state = 'OFF';
  92. my $stateLast = $state;
  93. my %exists = ();
  94. my %existsLast = %exists;
  95. my $pushLast = 0;
  96. my $pullLast = time();
  97. my $update = 0;
  98. my @COLOR = ();
  99. my $colorChange = time();
  100. my $PID = undef();
  101. my $EFFECT = undef();
  102. my $PID_DATA = undef();
  103. # Always force lights out at launch
  104. DMX::dim({ 'channel' => 13, 'value' => 0, 'time' => 0 });
  105. DMX::dim({ 'channel' => 14, 'value' => 0, 'time' => 0 });
  106. DMX::dim({ 'channel' => 15, 'value' => 0, 'time' => 0 });
  107. # Loop forever
  108. while (1) {
  109. # Record the last state/exists data for diffs/resets
  110. $stateLast = $state;
  111. %existsLast = %exists;
  112. # State is calculated; use newState to gather data
  113. my $newState = $state;
  114. # Reduce the select delay when we're processing background effects
  115. # We don't want to hang waiting for event updates
  116. my $delay = $DELAY;
  117. if (defined($PID)) {
  118. $delay = 0.001;
  119. }
  120. # Wait for state updates
  121. my $cmdState = DMX::readState($delay, \%exists, \%VALID);
  122. if (defined($cmdState)) {
  123. $newState = $cmdState;
  124. $pullLast = time();
  125. }
  126. # Handle "STOP" commands
  127. if ($newState eq 'STOP') {
  128. if (defined($PID)) {
  129. if ($DEBUG) {
  130. print STDERR "Ending background processing\n";
  131. }
  132. kill(SIGTERM, $PID);
  133. }
  134. $newState = $state;
  135. $update = 1;
  136. }
  137. # Reap zombie children
  138. if (defined($PID)) {
  139. my $kid = waitpid($PID, WNOHANG);
  140. if ($kid > 0) {
  141. if ($DEBUG) {
  142. print STDERR 'Reaped child: ' . $PID . "\n";
  143. }
  144. # Forget our local bypass state
  145. $PID = undef();
  146. $EFFECT = undef();
  147. $PID_DATA = undef();
  148. # Clear the RAVE flag for other daemons
  149. if (-e $RAVE_FILE) {
  150. unlink($RAVE_FILE);
  151. }
  152. # Reset to standard
  153. @COLOR = ();
  154. $colorChange = time() + $COLOR_TIME_MIN;
  155. $update = 1;
  156. }
  157. }
  158. # Continue special processing
  159. if (defined($EFFECT)) {
  160. $EFFECT->();
  161. next;
  162. }
  163. # Special handling for effects states
  164. if (defined($EFFECTS{$newState})) {
  165. # Dispatch the handler
  166. # Optionally skip the rest of this loop
  167. if ($EFFECTS{$newState}->()) {
  168. next;
  169. }
  170. # Force an update back to the original state
  171. $newState = $stateLast;
  172. %exists = %existsLast;
  173. @COLOR = ();
  174. $colorChange = time() + $COLOR_TIME_MIN;
  175. $update = 1;
  176. }
  177. # Calculate the new state
  178. if ($exists{'LIGHTS'}) {
  179. if ($newState eq 'PLAY') {
  180. $newState = 'PLAY_HIGH';
  181. }
  182. } else {
  183. if ($newState eq 'PLAY_HIGH') {
  184. $newState = 'PLAY';
  185. }
  186. }
  187. $state = $newState;
  188. # Color changes
  189. if ($COLOR_VAR{$state} && time() - $colorChange > $COLOR_TIMEOUT) {
  190. @COLOR = ();
  191. # Grab the default (white) data
  192. my $lums = 0;
  193. foreach my $data (@{ $DIM{$state} }) {
  194. $lums += $data->{'value'};
  195. }
  196. my $numChans = scalar(@{ $DIM{$state} });
  197. my $max = $lums / $numChans;
  198. # Pick the change interval
  199. my $time = int((rand($COLOR_TIMEOUT - $COLOR_TIME_MIN) + $COLOR_TIME_MIN) * 1000);
  200. # Assign each channel
  201. my @vals = random_normal($numChans, $max, $max * $COLOR_VAR{$state});
  202. foreach my $data (@{ $DIM{$state} }) {
  203. my $color = pop(@vals);
  204. push(@COLOR, { 'channel' => $data->{'channel'}, 'value' => $color, 'time' => $time });
  205. }
  206. # Update
  207. $update = 1;
  208. $colorChange = time();
  209. if ($DEBUG) {
  210. print STDERR "New color\n";
  211. }
  212. }
  213. # Force updates on a periodic basis
  214. if (time() - $pushLast > $PUSH_TIMEOUT) {
  215. $update = 1;
  216. }
  217. # Die if we don't see regular updates
  218. if (time() - $pullLast > $PULL_TIMEOUT) {
  219. die('No update on state socket in past ' . $PULL_TIMEOUT . " seconds. Exiting...\n");
  220. }
  221. # Force updates on any state change
  222. if ($stateLast ne $state) {
  223. if ($DEBUG) {
  224. print STDERR 'State change: ' . $stateLast . ' => ' . $state . "\n";
  225. }
  226. $update = 1;
  227. # Reset the color change sequence, so we always spend 1 cycle at white
  228. @COLOR = ();
  229. $colorChange = time() + $COLOR_TIME_MIN;
  230. }
  231. # Update the lighting
  232. if ($update) {
  233. # Select a data set (color or standard)
  234. my @data_set = ();
  235. my $local_state = $state;
  236. if (scalar(@COLOR)) {
  237. @data_set = @COLOR;
  238. $local_state .= ' (Color)';
  239. } else {
  240. @data_set = @{ $DIM{$state} };
  241. }
  242. # Update
  243. DMX::applyDataset(\@data_set, $local_state, $OUTPUT_FILE);
  244. # Update the push time
  245. $pushLast = time();
  246. # Clear the update flag
  247. $update = 0;
  248. # Clear the RAVE file, just in case
  249. if (-e $RAVE_FILE) {
  250. unlink($RAVE_FILE);
  251. die("Cleared orphan RAVE file\n");
  252. }
  253. }
  254. }
  255. # ======================================
  256. # Effects routines
  257. # These are blocking, so be careful
  258. # ======================================
  259. sub red_alert() {
  260. if ($DEBUG) {
  261. print STDERR "red_alert()\n";
  262. }
  263. my $file = '/mnt/media/DMX/Red Alert.mp3';
  264. my @sound = ('afplay', $file);
  265. my $ramp = 450;
  266. my $sleep = $ramp;
  267. # Stat the file to bring the network up-to-date
  268. stat($file);
  269. # Bring the B & G channels down to 0
  270. my @other = ();
  271. push(@other, { 'channel' => 13, 'value' => 0, 'time' => 0 });
  272. push(@other, { 'channel' => 15, 'value' => 0, 'time' => 0 });
  273. foreach my $data (@other) {
  274. DMX::dim($data);
  275. }
  276. my %high = ('channel' => 14, 'value' => 255, 'time' => $ramp);
  277. my %low = %high;
  278. $low{'value'} = 64;
  279. DMX::dim(\%high);
  280. system(@sound);
  281. DMX::dim(\%low);
  282. usleep($sleep * 1000);
  283. DMX::dim(\%high);
  284. system(@sound);
  285. DMX::dim(\%low);
  286. usleep($sleep * 1000);
  287. DMX::dim(\%high);
  288. system(@sound);
  289. DMX::dim(\%low);
  290. usleep($sleep * 1000);
  291. # Follow through on the loop
  292. return 0;
  293. }
  294. sub rave() {
  295. if ($DEBUG) {
  296. print STDERR "rave()\n";
  297. }
  298. my $file = '/mnt/media/DMX/Rave.mp3';
  299. my @sound = ('afplay', $file);
  300. # Stat the file to bring the network up-to-date
  301. stat($file);
  302. # Play the sound in a child (i.e. in the background)
  303. $PID = fork();
  304. if (defined($PID) && $PID == 0) {
  305. exec(@sound)
  306. or die('Unable to play sound: ' . join(' ', @sound) . "\n");
  307. }
  308. # Setup our loop handler
  309. $EFFECT = \&rave_loop;
  310. # Save data for future runs
  311. my %data = ();
  312. $PID_DATA = \%data;
  313. # Record our start time
  314. $data{'start'} = Time::HiRes::time();
  315. # Initialize the channels hash
  316. my %channels = (
  317. 1 => 0,
  318. 2 => 0,
  319. 4 => 0,
  320. 5 => 0,
  321. 6 => 0,
  322. 7 => 0,
  323. 8 => 0,
  324. 9 => 0,
  325. 13 => 0,
  326. 14 => 0,
  327. 15 => 0,
  328. );
  329. $data{'channels'} = \%channels;
  330. $data{'num_channels'} = scalar(keys(%channels));
  331. $data{'live_channels'} = 0;
  332. # Initiate the RAVE state
  333. touch($RAVE_FILE);
  334. # Do not pass GO, do not collect $200
  335. return 1;
  336. }
  337. sub rave_loop() {
  338. if ($DEBUG) {
  339. print STDERR "rave_loop()\n";
  340. }
  341. # Config
  342. my $max_dur = 350;
  343. my $max_val = 255;
  344. my $reserve = 0.75;
  345. my $ramp_dur = 10.15;
  346. my $hit_pos = 43.15;
  347. my $hit_dur = 100;
  348. my $fade_pos = 43.65;
  349. my $fade_dur = 2000;
  350. # How long have we been playing
  351. my $elapsed = Time::HiRes::time() - $PID_DATA->{'start'};
  352. if ($DEBUG) {
  353. print STDERR 'Elapsed: ' . $elapsed . "\n";
  354. }
  355. # Ramp up the number of channels in our effect
  356. if ($elapsed < $ramp_dur) {
  357. my $ratio = $PID_DATA->{'live_channels'} / ($PID_DATA->{'num_channels'} * (1 - $reserve));
  358. if ($PID_DATA->{'live_channels'} < 1 || $ratio < $elapsed / $ramp_dur) {
  359. my @keys = keys(%{ $PID_DATA->{'channels'} });
  360. my $index = int(rand($PID_DATA->{'num_channels'}));
  361. while ($PID_DATA->{'channels'}->{ $keys[$index] } > 0) {
  362. $index = int(rand($PID_DATA->{'num_channels'}));
  363. }
  364. $PID_DATA->{'channels'}->{ $keys[$index] } = 1;
  365. $PID_DATA->{'live_channels'}++;
  366. }
  367. } else {
  368. if ($PID_DATA->{'live_channels'} < $PID_DATA->{'num_channels'}) {
  369. foreach my $chan (keys(%{ $PID_DATA->{'channels'} })) {
  370. $PID_DATA->{'channels'}->{$chan} = 1;
  371. }
  372. $PID_DATA->{'live_channels'} = $PID_DATA->{'num_channels'};
  373. }
  374. }
  375. # Prepare data for each channel
  376. my @data_set = ();
  377. my $wait = $max_dur;
  378. foreach my $chan (keys(%{ $PID_DATA->{'channels'} })) {
  379. my $val = 0;
  380. my $dur = 0;
  381. # Random data on enabled channels until the fade
  382. if ($elapsed < $hit_pos) {
  383. if ($PID_DATA->{'channels'}->{$chan} > 0) {
  384. $dur = int(rand($max_dur));
  385. $val = int(rand($max_val));
  386. }
  387. } elsif ($elapsed < $fade_pos) {
  388. $val = $max_val;
  389. $dur = $hit_dur;
  390. $wait = ($fade_pos - $hit_pos) * 1000;
  391. } else {
  392. $val = 0;
  393. $dur = $fade_dur;
  394. $wait = $dur;
  395. }
  396. push(@data_set, { 'channel' => $chan, 'value' => $val, 'time' => $dur });
  397. }
  398. # Push the data set
  399. DMX::applyDataset(\@data_set, 'RAVE', $OUTPUT_FILE);
  400. $pushLast = time();
  401. # Wait for the fade interval
  402. usleep($wait * 1000);
  403. }