db-frontend.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. <?php
  2. require_once("rdp.php");
  3. require_once("environmental.php");
  4. require_once("fantastic.php");
  5. /* we ended up with two tables for storing captured sensor data
  6. *
  7. * the existing users of the `bosch_bme680` table are supposed to provide
  8. * start and end WHERE clause. each row has several possible interesting
  9. * columns which makes it a bit harder to make queries against
  10. *
  11. * the original table, `radonLog` has the one column that we are most
  12. * interested in. one thing we should try to improve is that users request
  13. * all possible data since the radon data capture project started
  14. */
  15. class SensorFigureOuterThingForGettingSQL {
  16. public $sql;
  17. private function timeRestraints($start = false, $end = false) {
  18. $startVal = intval($start);
  19. $endVal = intval($end);
  20. $where = "";
  21. if(false !== $start) $where .= "time >= {$startVal} AND ";
  22. if(false !== $end) $where .= "time <= {$endVal} AND ";
  23. return $where;
  24. }
  25. public function __construct($sensor, $type = "radon", $start = false, $end = false) {
  26. // the default SQL query
  27. $sql = "
  28. SELECT time, reading
  29. FROM radonLog
  30. WHERE {$this->timeRestraints($start, $end)} id='{$sensor->name}'
  31. ORDER BY time ASC
  32. ";
  33. // generate a modified query when getting data for BME680 sensors
  34. if("radon" !== $type) {
  35. $temp = $this->getQueryForBME680($sensor, $type, $start, $end);
  36. if(false !== $temp) $sql = $temp;
  37. }
  38. $this->sql = $sql;
  39. }
  40. private function getQueryForBME680($sensor, $type, $start = false, $end = false) {
  41. // create a list of columns that are okay to query
  42. $okay_columns = explode(" ", BME680_COLUMNS);
  43. if(false !== ($k = array_search("time", $okay_columns)))
  44. unset($okay_columns[$k]);
  45. if(false !== ($k = array_search("sensor", $okay_columns)))
  46. unset($okay_columns[$k]);
  47. // cancel this query if type is invalid
  48. if(false === array_search($type, $okay_columns)) {
  49. error_log(
  50. "the requested type, {$type} must be one of: " .
  51. implode(',', $okay_columns)
  52. );
  53. return array(/* empty array for you */);
  54. }
  55. // the sensor that you want has a different name, I will look it up
  56. $sensorName = strval(eMa_sensorName($sensor->name));
  57. // prevent injection in time interval user input
  58. $and = $this->timeRestraints($start, $end);
  59. // add optional time interval to query
  60. $sql = "SELECT time, {$type} AS reading FROM `bosch_bme680` WHERE {$and}";
  61. // lookup by sensor, make sure query sorts results oldest event to newest
  62. $sql .= " sensor = {$sensorName} ORDER BY time ASC";
  63. return $sql;
  64. }
  65. }
  66. /* all the input data parameters is a data structure that is an array with
  67. * two elements with keys `0` and `1`
  68. *
  69. * the element at data[0] is an array representing x values
  70. * the element at data[1] is an array representing y values
  71. *
  72. * each element are supposed to be the same length - see older versions around
  73. * git commit sha: 3dcaa832dd99d0073198a041f7dc1d0c9a6b03a8, basically representing
  74. * x and y in separate arrays in PHP 5.3 used a lot less memory versus objects
  75. * or an array of x,y pairs
  76. */
  77. class DataSqueeze {
  78. public $sql;
  79. /* simple FIR is a rolling average, no actual convolution
  80. *
  81. * [!] for the Air Things sensors that require manual data entry it
  82. * is unreasonable to have readings at regular time intervals so an FIR
  83. * filter is not reasonable to apply
  84. *
  85. * the other sensors report data using data compression, they only send
  86. * data when it is different, this means that the filter cannot be applied
  87. * directly to the data, missing readings need to be generated and fed
  88. * into the filter to keep the output stable
  89. */
  90. static public function simpleFIR($data, $delay, $period) {
  91. // do not run FIR filter unless there is enough delay
  92. if(2 > $delay) return $data;
  93. /* In some cases we will get a request where not enough data exists
  94. * to fill the delay buffer, in these cases we should do something
  95. *
  96. * check if last time stamp happens before minimum amount of time
  97. * required for this filter
  98. */
  99. if(false) {
  100. // this test is not good, regenerated data can fill out the
  101. // delay buffer so testing the input data length is pointless
  102. $ctData = count($data[0]);
  103. if($ctData < $delay) {
  104. trigger_error(
  105. "Not enough data ({$ctData}) to fill FIR delay buffer ({$delay}).",
  106. E_USER_WARNING
  107. );
  108. // do no filtering at all?
  109. return $data;
  110. }
  111. }
  112. if(/* last */ end($data[0]) < /* min */ ($data[0][0] + ($delay * $period))) {
  113. trigger_error(
  114. "Last time stamp in data occurs before last FIR calculated timestamp.",
  115. E_USER_WARNING
  116. );
  117. // do no filtering at all?
  118. return $data;
  119. }
  120. /* output will have some phase shift, (N - 1) / (2 * Fs)
  121. * Fs is sample frequency, N is taps (delay)
  122. *
  123. * since data uses "time" in seconds the shift should be an integer,
  124. * which will cause the output curves to be noticeably off.
  125. * regardless, the phase shift would be far greater without this
  126. * additional step
  127. */
  128. $shift = intval(($delay - 1) / (2 * (1 / $period)));
  129. // output is possibly going to be bigger than input
  130. $buf = array();
  131. $out = array(array(/* x value array */), array(/* y value array */));
  132. $p = 0;
  133. /* [!] confusing code ahead...
  134. *
  135. * the use of the `$o` variable to track the offset / indices of the
  136. * `$data` array is a performance optimization
  137. *
  138. * -- keep a separate pointer for the stored data, using `array_shift`
  139. * on an array with 100K elements is going to bring pretty big
  140. * consequences
  141. */
  142. $o = 0;
  143. $total = count($data[0]);
  144. $lastReading = 0;
  145. /* when recording every single value we run out of memory
  146. *
  147. * it is sufficient to store the filtered values as the buffer goes
  148. * into a stable state (until all values in buffer are equal), and
  149. * then to stop recording until just before the buffer starts to change
  150. * again. there is some importance to the `just before` as we need to
  151. * duplicate the value when we stopped recording as well as record the
  152. * value that signaled us to resume recording
  153. */
  154. $stoppedRecording = false;
  155. $lastStored = NULL;
  156. $lastTimestamp = NULL;
  157. // grab initial timestamp, also rewind one period to make loop easier
  158. $t = $data[/*time*/ 0][0] - $period;
  159. $lastMem = 0;
  160. $warningCounter = 0;
  161. // re-create time, decompress data, regenerate missing data points
  162. while($o < $total) {
  163. // advance time (initial time is t - 1)
  164. $t += $period;
  165. // get value of the "next" timestamp
  166. $n = $data[/*time*/ 0][$o];
  167. // is the next timestamp less than a period away?
  168. if($period > ($n - $t)) {
  169. /* just use the next timestamp, there may be some temporal
  170. * shift the further away it is, but it should be minimal
  171. *
  172. * in most cases it'll be just right, shift will be zero and
  173. * we will consume one from the queue
  174. */
  175. $t = $n;
  176. $lastReading = $data[/*reading*/ 1][$o];
  177. $o++;
  178. }
  179. // store reading into ring buffer
  180. $buf[$p++ % $delay] = $lastReading;
  181. // delay is how many samples are buffered before output
  182. if($p < $delay) continue;
  183. /* warn users when they start wanting a million things to be considered
  184. * for inclusion into the filter, this does not mean all will be stored
  185. * but that they are asking for probably too much
  186. *
  187. * a sensor might take a reading every 5 minutes, this works out to
  188. * 1,048,320 readings every ten years, not sure why someone would
  189. * ask us to process that much data....
  190. */
  191. if(1000000 === $warningCounter++) {
  192. error_log(implode(" ", array("File:", __FILE__, __METHOD__)) .
  193. ": I am not sure you are using this right...");
  194. }
  195. // manually force collection of garbage cycles every 10K iterations
  196. //if(0 === ($warningCounter % 10000)) gc_collect_cycles();
  197. // cache the value we should store, in case it takes time to compute
  198. $whatWeShouldStore = array_sum($buf) / $delay;
  199. // should we start recording?
  200. if((true === $stoppedRecording) and ($whatWeShouldStore !== $lastStored)) {
  201. $stoppedRecording = false;
  202. // make a new recording at the last timestamp
  203. $out[0][] = /*time*/ $lastTimestamp /* adjust for phase shift */ - $shift;
  204. $out[1][] = /*reading*/ $lastStored;
  205. }
  206. // we may use the current timestamp in the future
  207. $lastTimestamp = $t;
  208. // should we stop recording?
  209. if($whatWeShouldStore === $lastStored) {
  210. $stoppedRecording = true;
  211. continue;
  212. } // else
  213. // store all readings from filter, after delay is surpassed
  214. $out[0][] = /*time*/ $lastTimestamp /* adjust for phase shift */ - $shift;
  215. $out[1][] = /*reading*/ $whatWeShouldStore;
  216. $lastStored = $whatWeShouldStore;
  217. }
  218. /* when the filter becomes stable (all values in delay buffer are equal
  219. * we end up truncating the data points on accident, to make sure that
  220. * doesn't happen check if it happened now that the we are 'done'
  221. */
  222. if($stoppedRecording) {
  223. $out[0][] = /*time*/ $lastTimestamp /* adjust for phase shift */ - $shift;
  224. $out[1][] = /*reading*/ $lastStored;
  225. }
  226. if(defined("USE_SMALLER_FLOATS")) {
  227. /* convert all floating point to integer, keep two digits of
  228. * base-ten deciaml
  229. */
  230. for($i = count($out[1]); $i--;) $out[1][$i] = $out[1][$i] / 100.0;
  231. }
  232. if(defined("USE_INTEGER_MATHS")) {
  233. /* convert all floating point to integer, keep two digits of
  234. * base-ten deciaml
  235. */
  236. for($i = count($out[1]); $i--;) $out[1][$i] = intval($out[1][$i] * 100);
  237. }
  238. return $out;
  239. }
  240. /* I think this is just an example, it is difficult to use due to it
  241. * doing too much stuff, for example it runs a filter for us and formats
  242. * the output data probably for some graphical rendering software
  243. */
  244. public function getRDP($sensor, $type = "radon", $start = false, $end = false) {
  245. $rows = $this->getRows($sensor, $type, $start, $end);
  246. var_dump("rows_initial", count($rows));
  247. $delay = $sensor->options->getSampleDepth();
  248. $period = $sensor->options->getSamplePeriod();
  249. // convert rows to array of [x,y]
  250. $curve = array();
  251. foreach($rows as $r) $curve[] = array($r["time"], $r["reading"]);
  252. // run data through the simplest of FIR filters
  253. $rows = $this->simpleFIR($curve, $delay, $period);
  254. // maybe convert floats to integer?
  255. for($i = count($rows); $i--;) $rows[$i][1] = intval($rows[$i][1]);
  256. var_dump("rows_filtered", count($rows));
  257. $rdp = new RamerDouglasPeucker($rows);
  258. $epsilon = $sensor->options->getSquishFactor($type);
  259. if(defined("USE_INTEGER_MATHS")) {
  260. $epsilon = intval($epsilon * 100);
  261. }
  262. if(defined("USE_SMALLER_FLOATS")) {
  263. $epsilon /= 100.0;
  264. }
  265. // provide epsilon for Ramer Douglas Peucker
  266. $rows = $rdp->getRDP($epsilon);
  267. var_dump("rows_rdp", count($rows));
  268. // re-key data for end user
  269. foreach($rows as $k => $v) $rows[$k] = array("time" => $v[0], "reading" => $v[1]);
  270. return $rows;
  271. }
  272. // this is the routine that will be called externally, probably
  273. public function getRadon() { /* this is the original method */ }
  274. // getting rows from radon sensors pulls from the `radonLog` table
  275. public function getRows($sensor, $type = "radon", $start = false, $end = false) {
  276. $query = new SensorFigureOuterThingForGettingSQL($sensor, $type, $start, $end);
  277. // store SQL query for future inspection
  278. $this->sql = $query->sql;
  279. # connect to mysql database radondb
  280. $db = mysql_connect("localhost", "root", "secom8703");
  281. mysql_select_db("radondb", $db);
  282. if(false === ($result = mysql_query($query->sql, $db))) { return false; }
  283. $rows = array();
  284. while($row = mysql_fetch_array($result, MYSQL_ASSOC)) {
  285. /* [!] for whatever reason `time` and 'reading' are stored
  286. * (correctly) as INTEGER but returned as a TEXT by this routine,
  287. * this is terrible
  288. *
  289. * [!!] rows was using more than 50 MegaBytes for a year's worth
  290. * of readings from a RadonEye that sampled every five minutes
  291. * at least with PHP 5.3, storing the data as an array of x
  292. * and an array of y is more efficient than an array of [x,y]
  293. */
  294. $rows[/* x values - or time, whatever */ 0][] = intval($row["time"]);
  295. $rows[/* y values - or reading... */ 1][] = intval($row["reading"]);
  296. }
  297. mysql_free_result($result);
  298. # mysql needs clean-up
  299. mysql_close($db);
  300. return $rows;
  301. }
  302. public function getStdDeviation($sensor, $type = "radon", $start = false, $end = false) {
  303. $rows = $this->getRows($sensor, $type, $start, $end);
  304. //for($i = count($rows); $i--;) $rows[$i] = $rows[$i]["reading"];
  305. $rows = $rows[1];
  306. $n = count($rows);
  307. if(0 === $n) return false;
  308. $mean = array_sum($rows) / $n;
  309. $carry = 0.0;
  310. foreach($rows as $v) {
  311. $d = ((double) $v) - $mean;
  312. $carry += pow($d, 2);
  313. }
  314. return sqrt($carry / $n);
  315. }
  316. }