diff options
Diffstat (limited to 'localwebsite/classes')
-rw-r--r-- | localwebsite/classes/E3372.php | 292 | ||||
-rw-r--r-- | localwebsite/classes/GPIORelaydClient.php | 18 | ||||
-rw-r--r-- | localwebsite/classes/InverterdClient.php | 69 | ||||
-rw-r--r-- | localwebsite/classes/MyOpenWrtUtils.php | 133 | ||||
-rw-r--r-- | localwebsite/classes/MySimpleSocketClient.php | 90 | ||||
-rw-r--r-- | localwebsite/classes/Si7021dClient.php | 31 |
6 files changed, 633 insertions, 0 deletions
diff --git a/localwebsite/classes/E3372.php b/localwebsite/classes/E3372.php new file mode 100644 index 0000000..538d387 --- /dev/null +++ b/localwebsite/classes/E3372.php @@ -0,0 +1,292 @@ +<?php + +class E3372 +{ + + const WIFI_CONNECTING = '900'; + const WIFI_CONNECTED = '901'; + const WIFI_DISCONNECTED = '902'; + const WIFI_DISCONNECTING = '903'; + + const CRADLE_CONNECTING = '900'; + const CRADLE_CONNECTED = '901'; + const CRADLE_DISCONNECTED = '902'; + const CRADLE_DISCONNECTING = '903'; + const CRADLE_CONNECTFAILED = '904'; + const CRADLE_CONNECTSTATUSNULL = '905'; + const CRANDLE_CONNECTSTATUSERRO = '906'; + + const MACRO_EVDO_LEVEL_ZERO = '0'; + const MACRO_EVDO_LEVEL_ONE = '1'; + const MACRO_EVDO_LEVEL_TWO = '2'; + const MACRO_EVDO_LEVEL_THREE = '3'; + const MACRO_EVDO_LEVEL_FOUR = '4'; + const MACRO_EVDO_LEVEL_FIVE = '5'; + + // CurrentNetworkType + const MACRO_NET_WORK_TYPE_NOSERVICE = 0; + const MACRO_NET_WORK_TYPE_GSM = 1; + const MACRO_NET_WORK_TYPE_GPRS = 2; + const MACRO_NET_WORK_TYPE_EDGE = 3; + const MACRO_NET_WORK_TYPE_WCDMA = 4; + const MACRO_NET_WORK_TYPE_HSDPA = 5; + const MACRO_NET_WORK_TYPE_HSUPA = 6; + const MACRO_NET_WORK_TYPE_HSPA = 7; + const MACRO_NET_WORK_TYPE_TDSCDMA = 8; + const MACRO_NET_WORK_TYPE_HSPA_PLUS = 9; + const MACRO_NET_WORK_TYPE_EVDO_REV_0 = 10; + const MACRO_NET_WORK_TYPE_EVDO_REV_A = 11; + const MACRO_NET_WORK_TYPE_EVDO_REV_B = 12; + const MACRO_NET_WORK_TYPE_1xRTT = 13; + const MACRO_NET_WORK_TYPE_UMB = 14; + const MACRO_NET_WORK_TYPE_1xEVDV = 15; + const MACRO_NET_WORK_TYPE_3xRTT = 16; + const MACRO_NET_WORK_TYPE_HSPA_PLUS_64QAM = 17; + const MACRO_NET_WORK_TYPE_HSPA_PLUS_MIMO = 18; + const MACRO_NET_WORK_TYPE_LTE = 19; + const MACRO_NET_WORK_TYPE_EX_NOSERVICE = 0; + const MACRO_NET_WORK_TYPE_EX_GSM = 1; + const MACRO_NET_WORK_TYPE_EX_GPRS = 2; + const MACRO_NET_WORK_TYPE_EX_EDGE = 3; + const MACRO_NET_WORK_TYPE_EX_IS95A = 21; + const MACRO_NET_WORK_TYPE_EX_IS95B = 22; + const MACRO_NET_WORK_TYPE_EX_CDMA_1x = 23; + const MACRO_NET_WORK_TYPE_EX_EVDO_REV_0 = 24; + const MACRO_NET_WORK_TYPE_EX_EVDO_REV_A = 25; + const MACRO_NET_WORK_TYPE_EX_EVDO_REV_B = 26; + const MACRO_NET_WORK_TYPE_EX_HYBRID_CDMA_1x = 27; + const MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_0 = 28; + const MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_A = 29; + const MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_B = 30; + const MACRO_NET_WORK_TYPE_EX_EHRPD_REL_0 = 31; + const MACRO_NET_WORK_TYPE_EX_EHRPD_REL_A = 32; + const MACRO_NET_WORK_TYPE_EX_EHRPD_REL_B = 33; + const MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_0 = 34; + const MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_A = 35; + const MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_B = 36; + const MACRO_NET_WORK_TYPE_EX_WCDMA = 41; + const MACRO_NET_WORK_TYPE_EX_HSDPA = 42; + const MACRO_NET_WORK_TYPE_EX_HSUPA = 43; + const MACRO_NET_WORK_TYPE_EX_HSPA = 44; + const MACRO_NET_WORK_TYPE_EX_HSPA_PLUS = 45; + const MACRO_NET_WORK_TYPE_EX_DC_HSPA_PLUS = 46; + const MACRO_NET_WORK_TYPE_EX_TD_SCDMA = 61; + const MACRO_NET_WORK_TYPE_EX_TD_HSDPA = 62; + const MACRO_NET_WORK_TYPE_EX_TD_HSUPA = 63; + const MACRO_NET_WORK_TYPE_EX_TD_HSPA = 64; + const MACRO_NET_WORK_TYPE_EX_TD_HSPA_PLUS = 65; + const MACRO_NET_WORK_TYPE_EX_802_16E = 81; + const MACRO_NET_WORK_TYPE_EX_LTE = 101; + + + const ERROR_SYSTEM_NO_SUPPORT = 100002; + const ERROR_SYSTEM_NO_RIGHTS = 100003; + const ERROR_SYSTEM_BUSY = 100004; + const ERROR_LOGIN_USERNAME_WRONG = 108001; + const ERROR_LOGIN_PASSWORD_WRONG = 108002; + const ERROR_LOGIN_ALREADY_LOGIN = 108003; + const ERROR_LOGIN_USERNAME_PWD_WRONG = 108006; + const ERROR_LOGIN_USERNAME_PWD_ORERRUN = 108007; + const ERROR_LOGIN_TOUCH_ALREADY_LOGIN = 108009; + const ERROR_VOICE_BUSY = 120001; + const ERROR_WRONG_TOKEN = 125001; + const ERROR_WRONG_SESSION = 125002; + const ERROR_WRONG_SESSION_TOKEN = 125003; + + + private $host; + private $headers = []; + private $authorized = false; + private $useLegacyTokenAuth = false; + + public function __construct(string $host, bool $legacy_token_auth = false) { + $this->host = $host; + $this->useLegacyTokenAuth = $legacy_token_auth; + } + + public function __call(string $name, array $arguments) { + if (startsWith($name, 'get')) + $this->auth(); + + return call_user_func_array([$this, $name], $arguments); + } + + public function auth() { + if ($this->authorized) + return; + + if (!$this->useLegacyTokenAuth) { + $data = $this->request('webserver/SesTokInfo'); + $this->headers = [ + 'Cookie: '.$data['SesInfo'], + '__RequestVerificationToken: '.$data['TokInfo'], + 'Content-Type: text/xml' + ]; + } else { + $data = $this->request('webserver/token'); + $this->headers = [ + '__RequestVerificationToken: '.$data['token'], + 'Content-Type: text/xml' + ]; + } + $this->authorized = true; + } + + + // get* methods have to be protected for __call magic to work + + protected function getDeviceInformation() { + return $this->request('device/information'); + } + + protected function getDeviceSignal() { + return $this->request('device/signal'); + } + + protected function getMonitoringStatus() { + return $this->request('monitoring/status'); + } + + protected function getNotifications() { + return $this->request('monitoring/check-notifications'); + } + + protected function getDialupConnection() { + return $this->request('dialup/connection'); + } + + protected function getTrafficStats() { + return $this->request('monitoring/traffic-statistics'); + } + + protected function getSMSCount() { + return $this->request('sms/sms-count'); + } + + protected function getSMSList($page = 1, $count = 20) { + $xml = $this->request('sms/sms-list', 'POST', [ + 'PageIndex' => $page, + 'ReadCount' => $count, + 'BoxType' => 1, + 'SortType' => 0, + 'Ascending' => 0, + 'UnreadPreferred' => 1 + ], true); + $xml = simplexml_load_string($xml); + + $messages = []; + foreach ($xml->Messages->Message as $message) { + $messages[] = [ + 'date' => (string)$message->Date, + 'phone' => (string)$message->Phone, + 'content' => (string)$message->Content + ]; + } + return $messages; + } + + private function xmlToAssoc(string $xml): array { + $xml = new SimpleXMLElement($xml); + $data = []; + foreach ($xml as $name => $value) { + $data[$name] = (string)$value; + } + return $data; + } + + private function request(string $method, string $http_method = 'GET', array $data = [], bool $return_body = false) { + $ch = curl_init(); + $url = 'http://'.$this->host.'/api/'.$method; + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + if (!empty($this->headers)) + curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers); + if ($http_method == 'POST') { + curl_setopt($ch, CURLOPT_POST, true); + if (!empty($data)) + curl_setopt($ch, CURLOPT_POSTFIELDS, $this->postDataToXML($data)); + } + $body = curl_exec($ch); + + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($code != 200) + throw new Exception('e3372 host returned code '.$code); + + curl_close($ch); + return $return_body ? $body : $this->xmlToAssoc($body); + } + + private function postDataToXML(array $data, int $depth = 1) { + if ($depth == 1) + return '<?xml version: "1.0" encoding="UTF-8"?>'.$this->postDataToXML(['request' => $data], $depth+1); + + $items = []; + foreach ($data as $key => $value) { + if (is_array($value)) + $value = $this->postDataToXML($value, $depth+1); + $items[] = "<{$key}>{$value}</{$key}>"; + } + + return implode('', $items); + } + + public static function getNetworkTypeLabel($type): string { + switch ((int)$type) { + case self::MACRO_NET_WORK_TYPE_NOSERVICE: return 'NOSERVICE'; + case self::MACRO_NET_WORK_TYPE_GSM: return 'GSM'; + case self::MACRO_NET_WORK_TYPE_GPRS: return 'GPRS'; + case self::MACRO_NET_WORK_TYPE_EDGE: return 'EDGE'; + case self::MACRO_NET_WORK_TYPE_WCDMA: return 'WCDMA'; + case self::MACRO_NET_WORK_TYPE_HSDPA: return 'HSDPA'; + case self::MACRO_NET_WORK_TYPE_HSUPA: return 'HSUPA'; + case self::MACRO_NET_WORK_TYPE_HSPA: return 'HSPA'; + case self::MACRO_NET_WORK_TYPE_TDSCDMA: return 'TDSCDMA'; + case self::MACRO_NET_WORK_TYPE_HSPA_PLUS: return 'HSPA_PLUS'; + case self::MACRO_NET_WORK_TYPE_EVDO_REV_0: return 'EVDO_REV_0'; + case self::MACRO_NET_WORK_TYPE_EVDO_REV_A: return 'EVDO_REV_A'; + case self::MACRO_NET_WORK_TYPE_EVDO_REV_B: return 'EVDO_REV_B'; + case self::MACRO_NET_WORK_TYPE_1xRTT: return '1xRTT'; + case self::MACRO_NET_WORK_TYPE_UMB: return 'UMB'; + case self::MACRO_NET_WORK_TYPE_1xEVDV: return '1xEVDV'; + case self::MACRO_NET_WORK_TYPE_3xRTT: return '3xRTT'; + case self::MACRO_NET_WORK_TYPE_HSPA_PLUS_64QAM: return 'HSPA_PLUS_64QAM'; + case self::MACRO_NET_WORK_TYPE_HSPA_PLUS_MIMO: return 'HSPA_PLUS_MIMO'; + case self::MACRO_NET_WORK_TYPE_LTE: return 'LTE'; + case self::MACRO_NET_WORK_TYPE_EX_NOSERVICE: return 'NOSERVICE'; + case self::MACRO_NET_WORK_TYPE_EX_GSM: return 'GSM'; + case self::MACRO_NET_WORK_TYPE_EX_GPRS: return 'GPRS'; + case self::MACRO_NET_WORK_TYPE_EX_EDGE: return 'EDGE'; + case self::MACRO_NET_WORK_TYPE_EX_IS95A: return 'IS95A'; + case self::MACRO_NET_WORK_TYPE_EX_IS95B: return 'IS95B'; + case self::MACRO_NET_WORK_TYPE_EX_CDMA_1x: return 'CDMA_1x'; + case self::MACRO_NET_WORK_TYPE_EX_EVDO_REV_0: return 'EVDO_REV_0'; + case self::MACRO_NET_WORK_TYPE_EX_EVDO_REV_A: return 'EVDO_REV_A'; + case self::MACRO_NET_WORK_TYPE_EX_EVDO_REV_B: return 'EVDO_REV_B'; + case self::MACRO_NET_WORK_TYPE_EX_HYBRID_CDMA_1x: return 'HYBRID_CDMA_1x'; + case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_0: return 'HYBRID_EVDO_REV_0'; + case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_A: return 'HYBRID_EVDO_REV_A'; + case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_B: return 'HYBRID_EVDO_REV_B'; + case self::MACRO_NET_WORK_TYPE_EX_EHRPD_REL_0: return 'EHRPD_REL_0'; + case self::MACRO_NET_WORK_TYPE_EX_EHRPD_REL_A: return 'EHRPD_REL_A'; + case self::MACRO_NET_WORK_TYPE_EX_EHRPD_REL_B: return 'EHRPD_REL_B'; + case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_0: return 'HYBRID_EHRPD_REL_0'; + case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_A: return 'HYBRID_EHRPD_REL_A'; + case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_B: return 'HYBRID_EHRPD_REL_B'; + case self::MACRO_NET_WORK_TYPE_EX_WCDMA: return 'WCDMA'; + case self::MACRO_NET_WORK_TYPE_EX_HSDPA: return 'HSDPA'; + case self::MACRO_NET_WORK_TYPE_EX_HSUPA: return 'HSUPA'; + case self::MACRO_NET_WORK_TYPE_EX_HSPA: return 'HSPA'; + case self::MACRO_NET_WORK_TYPE_EX_HSPA_PLUS: return 'HSPA_PLUS'; + case self::MACRO_NET_WORK_TYPE_EX_DC_HSPA_PLUS: return 'DC_HSPA_PLUS'; + case self::MACRO_NET_WORK_TYPE_EX_TD_SCDMA: return 'TD_SCDMA'; + case self::MACRO_NET_WORK_TYPE_EX_TD_HSDPA: return 'TD_HSDPA'; + case self::MACRO_NET_WORK_TYPE_EX_TD_HSUPA: return 'TD_HSUPA'; + case self::MACRO_NET_WORK_TYPE_EX_TD_HSPA: return 'TD_HSPA'; + case self::MACRO_NET_WORK_TYPE_EX_TD_HSPA_PLUS: return 'TD_HSPA_PLUS'; + case self::MACRO_NET_WORK_TYPE_EX_802_16E: return '802_16E'; + case self::MACRO_NET_WORK_TYPE_EX_LTE: return 'LTE'; + default: return '?'; + } + } + +} diff --git a/localwebsite/classes/GPIORelaydClient.php b/localwebsite/classes/GPIORelaydClient.php new file mode 100644 index 0000000..89c8dc9 --- /dev/null +++ b/localwebsite/classes/GPIORelaydClient.php @@ -0,0 +1,18 @@ +<?php + +class GPIORelaydClient extends MySimpleSocketClient { + + const STATUS_ON = 'on'; + const STATUS_OFF = 'off'; + + public function setStatus(string $status) { + $this->send($status); + return $this->recv(); + } + + public function getStatus() { + $this->send('get'); + return $this->recv(); + } + +}
\ No newline at end of file diff --git a/localwebsite/classes/InverterdClient.php b/localwebsite/classes/InverterdClient.php new file mode 100644 index 0000000..b68b784 --- /dev/null +++ b/localwebsite/classes/InverterdClient.php @@ -0,0 +1,69 @@ +<?php + +class InverterdClient extends MySimpleSocketClient { + + /** + * @throws Exception + */ + public function setProtocol(int $v): string + { + $this->send("v $v"); + return $this->recv(); + } + + /** + * @throws Exception + */ + public function setFormat(string $fmt): string + { + $this->send("format $fmt"); + return $this->recv(); + } + + /** + * @throws Exception + */ + public function exec(string $command, array $arguments = []): string + { + $buf = "exec $command"; + if (!empty($arguments)) { + foreach ($arguments as $arg) + $buf .= " $arg"; + } + $this->send($buf); + return $this->recv(); + } + + /** + * @throws Exception + */ + public function recv() + { + $recv_buf = ''; + $buf = ''; + + while (true) { + $result = socket_recv($this->sock, $recv_buf, 1024, 0); + if ($result === false) + throw new Exception(__METHOD__ . ": socket_recv() failed: " . $this->getSocketError()); + + // peer disconnected + if ($result === 0) + break; + + $buf .= $recv_buf; + if (endsWith($buf, "\r\n\r\n")) + break; + } + + $response = explode("\r\n", $buf); + $status = array_shift($response); + if (!in_array($status, ['ok', 'err'])) + throw new Exception(__METHOD__.': unexpected status ('.$status.')'); + if ($status == 'err') + throw new Exception(empty($response) ? 'unknown inverterd error' : $response[0]); + + return trim(implode("\r\n", $response)); + } + +}
\ No newline at end of file diff --git a/localwebsite/classes/MyOpenWrtUtils.php b/localwebsite/classes/MyOpenWrtUtils.php new file mode 100644 index 0000000..6b70dbc --- /dev/null +++ b/localwebsite/classes/MyOpenWrtUtils.php @@ -0,0 +1,133 @@ +<?php + +class MyOpenWrtUtils { + + public static function getRoutingTable($table = null) { + $arguments = ['route-show']; + if ($table) + $arguments[] = $table; + + return self::toList(self::run($arguments)); + } + + public static function getRoutingRules() { + return self::toList(self::run(['rule-show'])); + } + + public static function ipsetList($set_name) { + return self::toList(self::run(['ipset-list', $set_name])); + } + + public static function ipsetListAll() { + global $config; + + $args = ['ipset-list-all']; + $args = array_merge($args, array_keys($config['modems'])); + + $lines = self::toList(self::run($args)); + + $sets = []; + $cur_set = null; + foreach ($lines as $line) { + if (startsWith($line, '>')) { + $cur_set = substr($line, 1); + if (!isset($sets[$cur_set])) + $sets[$cur_set] = []; + continue; + } + + if (is_null($cur_set)) { + debugError(__METHOD__.': cur_set is not set'); + continue; + } + + $sets[$cur_set][] = $line; + } + + return $sets; + } + + public static function ipsetAdd($set_name, $ip) { + return self::run(['ipset-add', $set_name, $ip]); + } + + public static function ipsetDel($set_name, $ip) { + return self::run(['ipset-del', $set_name, $ip]); + } + + public static function getDHCPLeases() { + $list = self::toList(self::run(['dhcp-leases'])); + $list = array_map('self::toDHCPLease', $list); + return $list; + } + + + // + // http functions + // + + private static function run(array $arguments) { + $url = self::getLink($arguments); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $body = curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($code != 200) + throw new Exception(__METHOD__.': http code '.$code); + + curl_close($ch); + return trim($body); + } + + private static function getLink($arguments) { + global $config; + + $url = 'http://'.$config['openwrt_ip'].'/cgi-bin/luci/command/cfg099944'; + if (!empty($arguments)) { + $arguments = array_map(function($arg) { + $arg = str_replace('/', '_', $arg); + return urlencode($arg); + }, $arguments); + $arguments = implode('%20', $arguments); + + $url .= '/'; + $url .= $arguments; + } + + // debugLog($url); + + return $url; + } + + + // + // parsing functions + // + + private static function toList(string $s): array { + if ($s == '') + return []; + return explode("\n", $s); + } + + private static function toDHCPLease(string $s): array { + $words = explode(' ', $s); + $time = array_shift($words); + $mac = array_shift($words); + $ip = array_shift($words); + array_pop($words); + $hostname = trim(implode(' ', $words)); + if (!$hostname) + $hostname = '?'; + return [ + 'time' => $time, + 'time_s' => date('d M, H:i:s', $time), + 'mac' => $mac, + 'ip' => $ip, + 'hostname' => $hostname + ]; + } + +}
\ No newline at end of file diff --git a/localwebsite/classes/MySimpleSocketClient.php b/localwebsite/classes/MySimpleSocketClient.php new file mode 100644 index 0000000..e59efba --- /dev/null +++ b/localwebsite/classes/MySimpleSocketClient.php @@ -0,0 +1,90 @@ +<?php + +class MySimpleSocketClient { + + protected $sock; + + public function __construct(string $host, int $port) + { + if (($socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false) + throw new Exception("socket_create() failed: ".$this->getSocketError()); + + $this->sock = $socket; + + if ((socket_connect($socket, $host, $port)) === false) + throw new Exception("socket_connect() failed: ".$this->getSocketError()); + } + + public function __destruct() + { + $this->close(); + } + + /** + * @throws Exception + */ + public function send(string $data) + { + $data .= "\r\n"; + $remained = strlen($data); + + while ($remained > 0) { + $result = socket_write($this->sock, $data); + if ($result === false) + throw new Exception(__METHOD__ . ": socket_write() failed: ".$this->getSocketError()); + + $remained -= $result; + if ($remained > 0) + $data = substr($data, $result); + } + } + + /** + * @throws Exception + */ + public function recv() + { + $recv_buf = ''; + $buf = ''; + + while (true) { + $result = socket_recv($this->sock, $recv_buf, 1024, 0); + if ($result === false) + throw new Exception(__METHOD__ . ": socket_recv() failed: " . $this->getSocketError()); + + // peer disconnected + if ($result === 0) + break; + + $buf .= $recv_buf; + if (endsWith($buf, "\r\n")) + break; + } + + return trim($buf); + } + + /** + * Close connection. + */ + public function close() + { + if (!$this->sock) + return; + + socket_close($this->sock); + $this->sock = null; + } + + /** + * @return string + */ + protected function getSocketError(): string + { + $sle_args = []; + if ($this->sock !== null) + $sle_args[] = $this->sock; + return socket_strerror(socket_last_error(...$sle_args)); + } + +}
\ No newline at end of file diff --git a/localwebsite/classes/Si7021dClient.php b/localwebsite/classes/Si7021dClient.php new file mode 100644 index 0000000..b976448 --- /dev/null +++ b/localwebsite/classes/Si7021dClient.php @@ -0,0 +1,31 @@ +<?php + +class Si7021dClient extends MySimpleSocketClient { + + public string $name; + public float $temp; + public float $humidity; + + /** + * @throws Exception + */ + public function __construct(string $host, int $port, string $name) { + parent::__construct($host, $port); + $this->name = $name; + + socket_set_timeout($this->sock, 3); + } + + public function readSensor(): void { + $this->send('read'); + + $data = jsonDecode($this->recv()); + + $temp = round((float)$data['temp'], 3); + $hum = round((float)$data['humidity'], 3); + + $this->temp = $temp; + $this->humidity = $hum; + } + +}
\ No newline at end of file |