leds.pl 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  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. # Die if we don't see regular updates
  127. if (time() - $pullLast > $PULL_TIMEOUT) {
  128. die('No update on state socket in past ' . $PULL_TIMEOUT . " seconds. Exiting...\n");
  129. }
  130. # Handle "STOP" commands
  131. if ($newState eq 'STOP') {
  132. if (defined($PID)) {
  133. if ($DEBUG) {
  134. print STDERR "Ending background processing\n";
  135. }
  136. kill(SIGTERM, $PID);
  137. }
  138. $newState = $state;
  139. $update = 1;
  140. }
  141. # Reap zombie children
  142. if (defined($PID)) {
  143. my $kid = waitpid($PID, WNOHANG);
  144. if ($kid > 0) {
  145. if ($DEBUG) {
  146. print STDERR 'Reaped child: ' . $PID . "\n";
  147. }
  148. # Forget our local bypass state
  149. $PID = undef();
  150. $EFFECT = undef();
  151. $PID_DATA = undef();
  152. # Clear the RAVE flag for other daemons
  153. if (-e $RAVE_FILE) {
  154. unlink($RAVE_FILE);
  155. }
  156. # Reset to standard
  157. @COLOR = ();
  158. $colorChange = time() + $COLOR_TIME_MIN;
  159. $update = 1;
  160. }
  161. }
  162. # Continue special processing
  163. if (defined($EFFECT)) {
  164. $EFFECT->();
  165. next;
  166. }
  167. # Special handling for effects states
  168. if (defined($EFFECTS{$newState})) {
  169. # %exists is usually bogus when in entering a special effect state
  170. my $existsTmp = \%exists;
  171. if (scalar(keys(%exists)) < 1) {
  172. $existsTmp = \%existsLast;
  173. }
  174. # Dispatch the handler
  175. # Optionally skip the rest of this loop
  176. if ($EFFECTS{$newState}->($existsTmp)) {
  177. next;
  178. }
  179. # Force an update back to the original state
  180. $newState = $stateLast;
  181. %exists = %existsLast;
  182. @COLOR = ();
  183. $colorChange = time() + $COLOR_TIME_MIN;
  184. $update = 1;
  185. }
  186. # Calculate the new state
  187. if ($exists{'LIGHTS'}) {
  188. if ($newState eq 'PLAY') {
  189. $newState = 'PLAY_HIGH';
  190. }
  191. } else {
  192. if ($newState eq 'PLAY_HIGH') {
  193. $newState = 'PLAY';
  194. }
  195. }
  196. $state = $newState;
  197. # Color changes
  198. if ($COLOR_VAR{$state} && time() - $colorChange > $COLOR_TIMEOUT) {
  199. @COLOR = ();
  200. # Grab the default (white) data
  201. my $lums = 0;
  202. foreach my $data (@{ $DIM{$state} }) {
  203. $lums += $data->{'value'};
  204. }
  205. my $numChans = scalar(@{ $DIM{$state} });
  206. my $max = $lums / $numChans;
  207. # Pick the change interval
  208. my $time = int((rand($COLOR_TIMEOUT - $COLOR_TIME_MIN) + $COLOR_TIME_MIN) * 1000);
  209. # Assign each channel
  210. my @vals = random_normal($numChans, $max, $max * $COLOR_VAR{$state});
  211. foreach my $data (@{ $DIM{$state} }) {
  212. my $color = pop(@vals);
  213. push(@COLOR, { 'channel' => $data->{'channel'}, 'value' => $color, 'time' => $time });
  214. }
  215. # Update
  216. $update = 1;
  217. $colorChange = time();
  218. if ($DEBUG) {
  219. print STDERR "New color\n";
  220. }
  221. }
  222. # Force updates on a periodic basis
  223. if (!$update && time() - $pushLast > $PUSH_TIMEOUT) {
  224. if ($DEBUG) {
  225. print STDERR "Forcing periodic update\n";
  226. }
  227. $update = 1;
  228. }
  229. # Force updates on any state change
  230. if (!$update && $stateLast ne $state) {
  231. if ($DEBUG) {
  232. print STDERR 'State change: ' . $stateLast . ' => ' . $state . "\n";
  233. }
  234. $update = 1;
  235. # Reset the color change sequence, so we always spend 1 cycle at white
  236. @COLOR = ();
  237. $colorChange = time() + $COLOR_TIME_MIN;
  238. }
  239. # Update the lighting
  240. if ($update) {
  241. # Select a data set (color or standard)
  242. my @data_set = ();
  243. my $local_state = $state;
  244. if (scalar(@COLOR)) {
  245. @data_set = @COLOR;
  246. $local_state .= ' (Color)';
  247. } else {
  248. @data_set = @{ $DIM{$state} };
  249. }
  250. # Update
  251. DMX::applyDataset(\@data_set, $local_state, $OUTPUT_FILE);
  252. # Update the push time
  253. $pushLast = time();
  254. # Clear the update flag
  255. $update = 0;
  256. # Clear the RAVE file, just in case
  257. if (-e $RAVE_FILE) {
  258. unlink($RAVE_FILE);
  259. die("Cleared orphan RAVE file\n");
  260. }
  261. }
  262. }
  263. # ======================================
  264. # Effects routines
  265. # These are blocking, so be careful
  266. # ======================================
  267. sub red_alert($) {
  268. my ($exists) = @_;
  269. if ($DEBUG) {
  270. print STDERR "red_alert()\n";
  271. }
  272. my $file = '/mnt/media/DMX/Red Alert.mp3';
  273. my @sound = ('afplay', $file);
  274. my $ramp = 450;
  275. my $sleep = $ramp;
  276. # Stat the file to bring the network up-to-date
  277. stat($file);
  278. # Bring the B & G channels down to 0
  279. my @other = ();
  280. push(@other, { 'channel' => 13, 'value' => 0, 'time' => 0 });
  281. push(@other, { 'channel' => 15, 'value' => 0, 'time' => 0 });
  282. foreach my $data (@other) {
  283. DMX::dim($data);
  284. }
  285. my %high = ('channel' => 14, 'value' => 255, 'time' => $ramp);
  286. my %low = %high;
  287. $low{'value'} = 64;
  288. DMX::dim(\%high);
  289. system(@sound);
  290. DMX::dim(\%low);
  291. usleep($sleep * 1000);
  292. DMX::dim(\%high);
  293. system(@sound);
  294. DMX::dim(\%low);
  295. usleep($sleep * 1000);
  296. DMX::dim(\%high);
  297. system(@sound);
  298. DMX::dim(\%low);
  299. usleep($sleep * 1000);
  300. # Follow through on the loop
  301. return 0;
  302. }
  303. sub rave($) {
  304. my ($exists) = @_;
  305. if ($DEBUG) {
  306. print STDERR "rave()\n";
  307. }
  308. # Config
  309. my $SND_APP = 'afplay';
  310. my $RAVE_MP3 = '/mnt/media/DMX/Rave.mp3';
  311. my $SILENCE = '/mnt/media/DMX/Silence.wav';
  312. my $SIL_DELAY = 1.3;
  313. my $AMP_SHORT = 5;
  314. my $AMP_LONG = $AMP_SHORT + 5;
  315. # Stat the file to bring the network up-to-date
  316. stat($SILENCE);
  317. stat($RAVE_MP3);
  318. # Initiate the RAVE state
  319. touch($RAVE_FILE);
  320. # Setup our loop handler
  321. $EFFECT = \&rave_loop;
  322. # Save data for future runs
  323. my %data = ();
  324. $PID_DATA = \%data;
  325. # Initialize the channels hash
  326. my %channels = (
  327. 1 => 0,
  328. 2 => 0,
  329. 4 => 0,
  330. 5 => 0,
  331. 6 => 0,
  332. 7 => 0,
  333. 8 => 0,
  334. 9 => 0,
  335. 13 => 0,
  336. 14 => 0,
  337. 15 => 0,
  338. );
  339. $data{'channels'} = \%channels;
  340. $data{'num_channels'} = scalar(keys(%channels));
  341. $data{'live_channels'} = 0;
  342. # Reduce the amp delay if the amp is already on
  343. my $amp_wait = $AMP_LONG;
  344. if ($exists->{'AMPLIFIER'}) {
  345. $amp_wait = $AMP_SHORT;
  346. }
  347. # Dim while we wait
  348. my @data_set = ();
  349. foreach my $chan (keys(%channels)) {
  350. push(@data_set, { 'channel' => $chan, 'value' => 0, 'time' => $amp_wait / 2 * 1000 });
  351. }
  352. DMX::applyDataset(\@data_set, 'RAVE', $OUTPUT_FILE);
  353. $pushLast = time();
  354. # Wait for the amp to power up
  355. if ($DEBUG) {
  356. print STDERR 'Waiting ' . $amp_wait . " seconds for the amp to boot\n";
  357. }
  358. sleep($amp_wait - $SIL_DELAY);
  359. # Play a short burst of silence to get all the audio in-sync
  360. my @sound = ($SND_APP, $SILENCE);
  361. system(@sound);
  362. # Play the sound in a child (i.e. in the background)
  363. $PID = fork();
  364. if (defined($PID) && $PID == 0) {
  365. my @sound = ($SND_APP, $RAVE_MP3);
  366. exec(@sound)
  367. or die('Unable to play sound: ' . join(' ', @sound) . "\n");
  368. }
  369. # Record our start time
  370. $data{'start'} = Time::HiRes::time();
  371. # Do not pass GO, do not collect $200
  372. return 1;
  373. }
  374. sub rave_loop() {
  375. if ($DEBUG) {
  376. print STDERR "rave_loop()\n";
  377. }
  378. # Config
  379. my $max_dur = 375;
  380. my $max_val = 255;
  381. my $reserve = 0.75;
  382. my $ramp_dur = 10.20;
  383. my $hit_pos = 43.15;
  384. my $hit_dur = 100;
  385. my $fade_pos = 43.65;
  386. my $fade_dur = 2000;
  387. # How long have we been playing
  388. my $elapsed = Time::HiRes::time() - $PID_DATA->{'start'};
  389. if ($DEBUG) {
  390. print STDERR 'Elapsed: ' . $elapsed . "\n";
  391. }
  392. # Ramp up the number of channels in our effect
  393. if ($elapsed < $ramp_dur) {
  394. my $ratio = $PID_DATA->{'live_channels'} / ($PID_DATA->{'num_channels'} * (1 - $reserve));
  395. if ($PID_DATA->{'live_channels'} < 1 || $ratio < $elapsed / $ramp_dur) {
  396. my @keys = keys(%{ $PID_DATA->{'channels'} });
  397. my $index = int(rand($PID_DATA->{'num_channels'}));
  398. while ($PID_DATA->{'channels'}->{ $keys[$index] } > 0) {
  399. $index = int(rand($PID_DATA->{'num_channels'}));
  400. }
  401. $PID_DATA->{'channels'}->{ $keys[$index] } = 1;
  402. $PID_DATA->{'live_channels'}++;
  403. }
  404. } else {
  405. if ($PID_DATA->{'live_channels'} < $PID_DATA->{'num_channels'}) {
  406. foreach my $chan (keys(%{ $PID_DATA->{'channels'} })) {
  407. $PID_DATA->{'channels'}->{$chan} = 1;
  408. }
  409. $PID_DATA->{'live_channels'} = $PID_DATA->{'num_channels'};
  410. }
  411. }
  412. # Prepare data for each channel
  413. my @data_set = ();
  414. my $wait = $max_dur;
  415. foreach my $chan (keys(%{ $PID_DATA->{'channels'} })) {
  416. my $val = 0;
  417. my $dur = 0;
  418. # Random data on enabled channels until the fade
  419. if ($elapsed < $hit_pos) {
  420. if ($PID_DATA->{'channels'}->{$chan} > 0) {
  421. $dur = int(rand($max_dur));
  422. $val = int(rand($max_val));
  423. }
  424. } elsif ($elapsed < $fade_pos) {
  425. $val = $max_val;
  426. $dur = $hit_dur;
  427. $wait = ($fade_pos - $hit_pos) * 1000;
  428. } else {
  429. $val = 0;
  430. $dur = $fade_dur;
  431. $wait = $dur;
  432. }
  433. push(@data_set, { 'channel' => $chan, 'value' => $val, 'time' => $dur });
  434. }
  435. # Push the data set
  436. DMX::applyDataset(\@data_set, 'RAVE', $OUTPUT_FILE);
  437. $pushLast = time();
  438. # Wait for the fade interval
  439. usleep($wait * 1000);
  440. }