index.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. <?php
  2. /* wrapper for curl, sends a single parameter to the esp8266, no attempt is made to interpret
  3. * user data, except that we protect curl from possible injection. Because protection is heavy
  4. * handed, end-user data can easily be mangled, as in, if it isn't formatted right it is going to
  5. * be forced through a correctly formatted die
  6. */
  7. function curl($postParam, $len, $route, $varName = "var") {
  8. $uri = "http://rain-gutter-rgb.lan.rome7.com/{$route}";
  9. $unsafeUserData = "";
  10. // start with an empty, off, or "zeroed" buffer
  11. for($i = $len; $i--;) $unsafeUserData .= "0";
  12. // [!] protect curl from injection, only allow hexidecimal digits 0..9a..fA..F]
  13. for($i = $len; $i--; ) $unsafeUserData[$i] = (isset($_POST[$postParam][$i]) && ctype_xdigit($_POST[$postParam][$i]))
  14. ? $_POST[$postParam][$i]
  15. : '0';
  16. $ch = curl_init();
  17. error_log("sending [{$postParam}] '{$unsafeUserData}' to {$uri}");
  18. curl_setopt($ch, CURLOPT_URL, $uri);
  19. curl_setopt($ch, CURLOPT_POST, 1);
  20. curl_setopt($ch, CURLOPT_POSTFIELDS, "{$postParam}={$unsafeUserData}");
  21. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  22. $out = curl_exec($ch);
  23. curl_close($ch);
  24. return $out;
  25. }
  26. /* check for a POST request and use curl to send data to the esp8266
  27. * default response is http bad request
  28. * passthru contains a simple description of the esp8266 api
  29. */
  30. if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  31. $code = 400; $out = "";
  32. $passthru = array(
  33. // POST_VARIABLE => [buffer-length, api-endpoint]
  34. "color" => array(6, "rgb"),
  35. "brightness" => array(2, "brightness"),
  36. "quickGamma" => array(1, "cfg"),
  37. "sCurveGamma" => array(1, "cfg"),
  38. "colorCorrection" => array(1, "cfg")
  39. );
  40. // execute the first matching command
  41. foreach($passthru as $k => $v) {
  42. if(!isset($_POST[$k])) continue; $out = curl($k, $v[0], $v[1]); $code = 200; break; }
  43. // set headers
  44. header("HTTP/1.0 " . strval($code) . (200 === $code ?" OK" : " Bad Request"));
  45. $GLOBALS["http_response_code"] = $code;
  46. //http_response_code($code);
  47. exit("{$out}");
  48. } /* else GET request, check for quick response parameters -- do not generate whole page */ {
  49. if(isset($_GET['req'])) {
  50. // default is to get color as decimal [0..255] values R,G,B
  51. $req = "dec";
  52. switch($_GET['req']) {
  53. case "color": $req = "dec"; break;
  54. case "brightness": $req = "brightness"; break;
  55. case "correction": $req = "cfg"; break;
  56. }
  57. $uri = "http://rain-gutter-rgb.lan.rome7.com/{$req}";
  58. // send GET request to the esp8266
  59. $ch = curl_init();
  60. error_log("sending [get: {$req}] via {$uri}");
  61. curl_setopt($ch, CURLOPT_URL, $uri);
  62. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  63. $out = curl_exec($ch);
  64. curl_close($ch);
  65. exit($out);
  66. }
  67. }
  68. // otherwise - generate a whole web-page
  69. echo "<html>";
  70. echo "<body>";
  71. // either use mixbox non-commercial use under the CC BY-NC 4.0 license
  72. echo "<script src='mixbox.js'></script>";
  73. // or the spectral.js MIT license
  74. echo "<script src='spectral.js'></script>";
  75. echo "
  76. <script>
  77. // example mixing two colors
  78. var rgb1 = 'rgb(0, 33, 133)'; // blue
  79. var rgb2 = 'rgb(252, 211, 0)'; // yellow
  80. var t = 0.5; // mixing ratio
  81. var mixed = mixbox.lerp(rgb1, rgb2, t);
  82. console.log(mixed);
  83. // example mixing multiple colors
  84. var z1 = mixbox.rgbToLatent(rgb1);
  85. var z2 = mixbox.rgbToLatent(rgb2);
  86. var z3 = mixbox.rgbToLatent(mixed);
  87. var zMix = new Array(mixbox.LATENT_SIZE);
  88. for (var i = 0; i < zMix.length; i++) { // mix:
  89. zMix[i] = (0.3*z1[i] + // 30% of rgb1
  90. 0.6*z2[i] + // 60% of rgb2
  91. 0.1*z3[i]); // 10% of mixed
  92. }
  93. var rgbMix = mixbox.latentToRgb(zMix);
  94. console.log(rgbMix);
  95. // using spectral.js to mix two colors (does not seem to be working..., returns [0,0,0])
  96. var color = spectral.mix('[0, 33, 133]', '[252, 211, 0]', 0.5, spectral.RGB);
  97. console.log(color);
  98. </script>
  99. ";
  100. // common functions
  101. echo "
  102. <script>
  103. // using the newer fetch API, not sure if this is worse than AJAX
  104. async function post(url = '', kw = 'key', data = 'data') {
  105. // to use _POST superblogal, send body as http multipart/form-data
  106. let fd = new FormData();
  107. fd.append(kw, data);
  108. return await fetch(url, { method: 'POST', body: fd });
  109. }
  110. /* I assume http get requests may use default
  111. * [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
  112. * parameters.
  113. */
  114. async function get(arg) { return await fetch('?' + new URLSearchParams({req:arg})).then((v) => v.text()); }
  115. function getColor(selectedObj = null) {
  116. // get the color from the esp8266, returns as decimal [0..255] R,G,B
  117. let dec = get('color');
  118. // split into red, green, and blue components:
  119. let rgb = dec.then((v) => { let a = v.split(',').map((i) => parseInt(i, 10)); return {red: a[0], green: a[1], blue: a[2]}; });
  120. rgb.then((v) => console.log('esp8266 color: ', v));
  121. // by default, return as Promise that will resolve to rgb object { red: x, green: y, blue: z }
  122. if(null === selectedObj) return rgb;
  123. // otherwise set backround and text of the selected color object
  124. rgb.then((v) => {
  125. let a = [v.red, v.green, v.blue];
  126. color = 'rgb(' + a.join(', ') + ')';
  127. let off = a.every((x) => 0 === x);
  128. // use gray as a placeholder for 'lights off'
  129. selectedObj.style.background = off ?'rgb(211, 211, 211)' :color;
  130. selectedObj.textContent = color;
  131. });
  132. }
  133. function getBrightness(sliderObj = null) {
  134. // get the brightness from the esp8266, returns as decimal [0..15]
  135. let brightness = get('brightness').then((v) => parseInt(v,10));
  136. brightness.then((i) => { console.log('esp8266 brightness: ', i); });
  137. // by default, return as Promise that will resolve to integer
  138. if(null === sliderObj) return brightness;
  139. // otherwise set slider object's value property to brightness when the promise resolves
  140. brightness.then((i) => { sliderObj.value = i; });
  141. }
  142. function getCorrection(inputs = null) {
  143. /* get the correction config from the esp8266, returns as cfg [ setting:[0..1], ... setting_n:[0..1]]
  144. * settings currently include:
  145. * quickGamma - corrects for human perception using quadratic function
  146. * sCurveGamma - corrects for human perception using sigmoid function
  147. * colorCorrection - LED manufacturer correction, differences between doping used for different colors
  148. *
  149. * + quickGamma and sCurveGamma can be turned on or off and are are mutually
  150. * exclusive to eachother
  151. * + colorCorrection may be turned on or off, and can be applied to either
  152. * gamma correction schemes
  153. */
  154. let cfg = get('correction');
  155. // split into config options
  156. let options = cfg.then((v) => {
  157. let temp = {};
  158. // convert to lower-case and keep only alpha-numeric
  159. v = v.toLowerCase().replace(/[^a-z0-9]/gi, '');
  160. temp.quickGamma = '0' !== v.split('quickgamma')[1][0];
  161. temp.sCurveGamma = '0' !== v.split('scurvegamma')[1][0];
  162. temp.colorCorrection = '0' !== v.split('colorcorrection')[1][0];
  163. // quick and sCurve are mutually exclusive, though they can both be off
  164. temp.noneGamma = temp.quickGamma === temp.sCurveGamma;
  165. return temp;
  166. });
  167. options.then((o) => console.log('esp8266 settings: ', o));
  168. // by default, return as Promise that will resolve to config options object
  169. if(null === inputs) return options;
  170. // otherwise set options
  171. for(let i = 0; i < inputs.length; i++) options.then((o) => {
  172. inputs[i].checked = Object.hasOwn(o, inputs[i].id) ?o[inputs[i].id] :false;
  173. });
  174. }
  175. </script>
  176. ";
  177. // render a color selection wheel
  178. echo "
  179. <script>
  180. var canvas = document.createElement('canvas');
  181. // google chrome performance flag
  182. //var context = canvas.getContext('2d');
  183. var context = canvas.getContext('2d', { willReadFrequently: true });
  184. var w = 400; var h = 400;
  185. canvas.width = w;
  186. canvas.height = h;
  187. for(var i = 0; i < 360; i += 0.1) {
  188. var rad = i * (2 * Math.PI) / 360;
  189. var x1 = w/2 + w/2 * Math.cos(rad);
  190. var y1 = h/2 + h/2 * Math.sin(rad);
  191. var gradient = context.createLinearGradient(w/2, h/2, x1, y1);
  192. gradient.addColorStop(0.0, 'white');
  193. gradient.addColorStop(1.0, 'hsla('+i+', 100%, 50%, 1.0)');
  194. context.strokeStyle = gradient;
  195. context.beginPath();
  196. context.moveTo(w/2, h/2);
  197. context.lineTo(x1, y1);
  198. context.stroke();
  199. }
  200. document.body.appendChild(canvas);
  201. </script>
  202. ";
  203. // simple color picker
  204. echo "
  205. <script>
  206. function tableData(c, head = false) {
  207. let td = document.createElement(head ?'th' :'td');
  208. if(typeof c === 'string' || c instanceof String) td.innerText = c;
  209. else td.appendChild(c);
  210. return td;
  211. }
  212. // create a table
  213. var table = document.createElement('table');
  214. var tableHead = document.createElement('thead');
  215. table.appendChild(tableHead);
  216. var tableHeadRow = document.createElement('tr');
  217. tableHead.appendChild(tableHeadRow);
  218. var tableBody = document.createElement('tbody');
  219. table.appendChild(tableBody);
  220. var tableBodyRow = document.createElement('tr');
  221. tableBody.appendChild(tableBodyRow);
  222. // add some headers
  223. tableHeadRow.appendChild(tableData('', true));
  224. tableHeadRow.appendChild(tableData('hover', true));
  225. tableHeadRow.appendChild(tableData('selected', true));
  226. // add table data
  227. tableBodyRow.appendChild(tableData(canvas));
  228. hovered = tableData(''); hovered.id = 'hovered-color';
  229. tableBodyRow.appendChild(hovered);
  230. selected = tableData(''); selected.id = 'selected-color';
  231. tableBodyRow.appendChild(selected);
  232. document.body.appendChild(table);
  233. // set the inital value of the selected color cell to the esp8266 lights
  234. getColor(selected);
  235. // listen to events from canvas
  236. function pick(event, destination, cb = false) {
  237. const bounding = canvas.getBoundingClientRect();
  238. const x = event.clientX - bounding.left;
  239. const y = event.clientY - bounding.top;
  240. const pixel = context.getImageData(x, y, 1, 1);
  241. const data = pixel.data;
  242. //console.log(pixel, data);
  243. const rgb = 'rgb(' + data[0] + ', ' + data[1] + ', ' + data[2] + ')';
  244. let off = 0 === (data[0] | data[1] | data[2]);
  245. // use 211,211,211 (gray) as a placeholder for 'lights turned off'
  246. destination.style.background = off ?'rgb(211, 211, 211)' :rgb;
  247. destination.textContent = rgb;
  248. // handle optional callback
  249. if(false !== cb) cb(data[0], data[1], data[2]);
  250. return rgb;
  251. }
  252. canvas.addEventListener('mousemove', (event) => pick(event, hovered));
  253. canvas.addEventListener('click', (event) => pick(event, selected));
  254. </script>
  255. ";
  256. // add a callback to the click event to send rgb value to esp8266
  257. echo "
  258. <script>
  259. function sg(x) { return Math.round((x*x)/255); }
  260. function callback(red, green, blue) {
  261. console.log('logged', red, green, blue);
  262. post(
  263. /* url */ '',
  264. /* key */ 'color',
  265. sg(red).toString(16).padStart(2,'0') +
  266. sg(green).toString(16).padStart(2,'0') +
  267. sg(blue).toString(16).padStart(2,'0')
  268. ).then((data) => { console.log(data, data.text()); });
  269. }
  270. canvas.removeEventListener('click', (event) => pick(event, selected));
  271. canvas.addEventListener('click', (event) => pick(event, selected, callback));
  272. </script>
  273. ";
  274. /* make a brightness slider, esp8266 takes 0..15 levels of brightness
  275. * zero is off and should be ignored
  276. *
  277. * currently brightness control is very simple and operates without
  278. * modifying end-user color choices
  279. *
  280. * tldr; users have 24bit color selectivity, in hardware we have a
  281. * little over 1.5 bytes per color giving us somewhere near 37bit color
  282. * selectivity. so a user can select 0a3bcc as their color choise and
  283. * we can have thousands of shades of colors that are close to that.
  284. * The power section of the esp8266 is not optimal so the esp8266 will
  285. * limit running the LEDs at maximum current. This limits the color-space
  286. * by about 20%. The way we implement brightness is to cut give the end
  287. * user 24-bits of color selectivity, that fits into 1/20th of the color-space
  288. * then we can multiply those values linearly 1..15 before we hit that 80%
  289. * power limit. because we are amplifying colors evenly the selected output
  290. * color will not change.
  291. */
  292. echo "
  293. <script>
  294. var slider = document.createElement('input');
  295. slider.type = 'range';
  296. slider.min = 1;
  297. slider.max = 15;
  298. slider.value = 7;
  299. slider.step = 1;
  300. var brightnessText = document.createElement('div');
  301. brightnessText.innerHTML = 'brightness';
  302. document.body.appendChild(brightnessText);
  303. document.body.appendChild(slider);
  304. // set the inital value of the slider to whatever the current brightness value
  305. getBrightness(slider);
  306. function slid(event, cb = false) {
  307. if(false !== cb) cb(slider.value);
  308. console.log('changed slider to ' + slider.value);
  309. return 'brightness(' + slider.value + ')';
  310. }
  311. slider.addEventListener('change', (event) => slid(event));
  312. </script>
  313. ";
  314. // add a callback to the off-click event on the slider
  315. echo "
  316. <script>
  317. function slidercb(valu) {
  318. console.log('logged', valu);
  319. post('', 'brightness', parseInt(valu, 10).toString(16).padStart(2,'0')).then((data) => { console.log(data, data.text()); });
  320. }
  321. slider.removeEventListener('change', (event) => slid(event));
  322. slider.addEventListener('change', (event) => slid(event, slidercb));
  323. </script>
  324. ";
  325. // add a checkbox and radio set to select config options for color correction
  326. echo "
  327. <script>
  328. function addChoice(inputs, parent, name, value, alternate = null, type = null) {
  329. var inp = document.createElement('input');
  330. inp.type = null === type ?'radio' :type;
  331. let f = value.replace(/[^a-zA-Z]/gi, '') + name.charAt(0).toUpperCase() + name.slice(1);
  332. inp.id = f;
  333. inp.name = name;
  334. inp.value = f;
  335. var lab = document.createElement('label');
  336. lab.for = f;
  337. lab.innerHTML = null === alternate ?value :alternate;
  338. parent.appendChild(inp);
  339. parent.appendChild(lab);
  340. inputs.push(inp);
  341. }
  342. function configChange(e) {
  343. /* check for noneGamma special radio button, unfortunately server does not
  344. * have an option to turn off both sCurveGamma and quickGamma, but we
  345. * want to provide this to end users...
  346. */
  347. if('noneGamma' === e.target.id) {
  348. post('', 'quickGamma', '0').then((d) => console.log(d.text()));
  349. post('', 'sCurveGamma', '0').then((d) => console.log(d.text()));
  350. } else {
  351. post('', e.target.id, e.target.checked ?'1' :'0').then((d) => console.log(d.text()));
  352. }
  353. //console.log(e);
  354. }
  355. var gamma = document.createElement('fieldest');
  356. var legend = document.createElement('legend');
  357. gamma.appendChild(legend);
  358. let inputs = [];
  359. addChoice(inputs, gamma, 'gamma', 'none');
  360. addChoice(inputs, gamma, 'gamma', 'sCurve');
  361. addChoice(inputs, gamma, 'gamma', 'quick');
  362. addChoice(inputs, gamma, 'correction', 'color', 'led-bias', 'checkbox');
  363. legend.innerHTML = 'color correction options';
  364. document.body.appendChild(gamma);
  365. // set config options from the esp8266
  366. getCorrection(inputs);
  367. // add listeners to each color correction config option
  368. for(let i = 0; i < inputs.length; i++) {
  369. inputs[i].addEventListener('change', (event) => configChange(event));
  370. }
  371. </script>
  372. ";
  373. echo "</body>";
  374. echo "</html>";