leds.pl 10 KB

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