commit d59f61d7649bbe0c0a24b6111fd092726aa97a63 Author: Ben Menking Date: Thu May 7 09:48:36 2026 -0400 initial clean commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..754f687 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vendor/* +.env diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..477c683 --- /dev/null +++ b/composer.json @@ -0,0 +1,20 @@ +{ + "name": "menking/meshcore-php", + "description": "A library for communicating with Meshcore USB companion firmware", + "type": "library", + "license": "MIT", + "autoload": { + "psr-4": { + "Menking\\Meshcore\\": "src/" + } + }, + "authors": [ + { + "name": "Ben Menking", + "email": "ben@menking.net" + } + ], + "require-dev": { + "phpstan/phpstan": "^2.1" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..e1bc4ac --- /dev/null +++ b/composer.lock @@ -0,0 +1,72 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "13f9192146b31c36272e50ca9ea50814", + "packages": [], + "packages-dev": [ + { + "name": "phpstan/phpstan", + "version": "2.1.54", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd", + "reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-04-29T13:31:09+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/src/CoreParser.php b/src/CoreParser.php new file mode 100644 index 0000000..f00203d --- /dev/null +++ b/src/CoreParser.php @@ -0,0 +1,297 @@ +code = ord($payload[0]); + $data->reserved = ord($payload[1]); + $data->tag = unpack('V', substr($payload, 2, 4))[1]; + $data->lpp = []; + + $lpp = substr($payload, 6); + // Cayenne LPP proto + $offset = 0; + while($offset < strlen($lpp) ) { + $block = substr($lpp, $offset, 4); + $q = [ + 'channel' => ord($block[0]), + 'type' => ord($block[1]), + 'type_name'=>CoreProtocol::getLppType(ord($block[1])), + 'value' => unpack('n', substr($block, 2, 2))[1] + ]; + + switch($q['type']) { + case CoreProtocol::LPP_VOLTAGE: + $q['value'] /= 100; + break; + case CoreProtocol::LPP_TEMP: + $q['value'] /= 10; + $q['fahrenheit'] = (($q['value'] * 9.0 / 5.0) + 32.0); + break; + } + + $offset += 4; + + if( $q['channel'] != 0 && $q['type'] != 0 ) { + $data->lpp[] = $q; + } + } + + return $data; + } + + /** + * + * @param string $payload + * @return ErrorResponse + */ + protected static function parseError(string $payload): ErrorResponse { + $e = new ErrorResponse(); + $e->code = ord($payload[0]); + $e->error_code = ord($payload[1]); + $e->error_text = CoreProtocol::getErrorText(ord($payload[1])); + + return $e; + } + + /** + * + * @param string $payload + * @return CurrentTimeResponse + */ + protected static function parseCurrentTime(string $payload): CurrentTimeResponse { + $data = new CurrentTimeResponse(); + + $data->code = ord($payload[0]); + $data->current_time = unpack('V', substr($payload, 1, 4))[1]; + + return $data; + } + + /** + * + * @param string $payload + * @return LoginResponse + */ + protected static function parseLoginFail(string $payload): LoginResponse { + $data = new LoginResponse(); + + $data->code = ord($payload[0]); + $data->status = false; + $data->is_admin = ord($payload[1]); + $data->pub_key_prefix = base64_encode(substr($payload, 2, 6)); + + return $data; + } + + /** + * + * @param string $payload + * @return LoginResponse + */ + protected static function parseLoginSuccess(string $payload): LoginResponse { + $data = new LoginResponse(); + + $data->code = ord($payload[0]); + $data->status = true; + $data->is_admin = ord($payload[1]); + $data->pub_key_prefix = base64_encode(substr($payload, 2, 6)); + + if( strlen($payload) > 8 ) { + $data->timestamp = unpack('V', substr($payload, 8, 4))[1]; + $data->acl = ord($payload[12]); + $data->firmware_version = ord($payload[13]); + } + + return $data; + } + + /** + * + * @param string $payload + * @return LogReceiveResponse + */ + protected static function parseLogRxData(string $payload): LogReceiveResponse { + $data = new LogReceiveResponse(); + + $data->code = ord($payload[0]); + $data->snr = ord($payload[1]) / 4; + $data->rssi = ord($payload[2]); + $data->log = base64_encode(substr($payload, 3, strlen($payload))); + + return $data; + } + + /** + * + * @param string $payload + * @return CodeSentResponse + */ + protected static function parseCodeSent(string $payload): CodeSentResponse { + $data = new CodeSentResponse(); + + $data->code = ord($payload[0]); + $data->sent_as_flood = ord($payload[1]); + $data->tag= unpack('V', substr($payload, 2, 4))[1]; + $data->est_timeout = unpack('V', substr($payload, 6, 4))[1]; + + return $data; + } + + /** + * + * @param string $payload + * @return DeviceInfoResponse + */ + protected static function parseDeviceInfo(string $payload): DeviceInfoResponse { + $data = new DeviceInfoResponse(); + + $data->code = ord($payload[0]); + $data->firmware_version = ord($payload[1]); + $data->max_contacts_raw = ord($payload[2]); + $data->max_channels = ord($payload[3]); + $data->ble_pin = unpack('v', substr($payload, 4, 4))[1]; + $data->firmware_build = trim(substr($payload, 8, 12)); + $data->model = trim(substr($payload, 20, 40)); + $data->version = trim(substr($payload, 60, 20)); + $data->client_repeat_enabled = ord($payload[80]); + $data->path_hash_mode = ord($payload[81]); + + return $data; + } + + /** + * + * @param string $payload + * @return ChannelResponse + */ + protected static function parseGetChannel(string $payload): ChannelResponse { + $detail = explode("\0", substr($payload, 2, 32)); + + $data = new ChannelResponse(); + + $data->code = ord($payload[0]); + $data->channel_idx = ord($payload[1]); + $data->detail = $detail; + $data->channel_secret = base64_encode(substr($payload, 34, 16)); + + return $data; + } + + /** + * + * @param string $payload + * @return BatteryStorageResponse + */ + protected static function parseBatteryAndStorage(string $payload): BatteryStorageResponse { + $data = new BatteryStorageResponse(); + + $data->code = ord($payload[0]); + $data->battery_millivolt = unpack('v', substr($payload, 1, 2))[1]; + $data->storage_used = unpack('V', substr($payload, 3, 4))[1]; + $data->storage_total = unpack('V', substr($payload, 7, 4))[1]; + + return $data; + } + + public static function getContact(string $payload) { + $contact = [ + 'code'=>ord($payload[0]), + 'pub_key'=>base64_encode(substr($payload, 1, CoreProtocol::PUB_KEY_SIZE)), + 'type'=>ord($payload[33]), + 'flags'=>ord($payload[34]), + 'out_path_len'=>ord($payload[35]), + 'out_path'=>trim(substr($payload, 36, 64)), + 'contact_name'=>trim(substr($payload, 100, 32)), + 'last_advert_timestamp'=>date('r', unpack('V', substr($payload, 132, 4))[1]), + 'gps_lat'=>unpack('l', substr($payload, 136, 4))[1] / 1000000.0, + 'gps_lon'=>unpack('l', substr($payload, 140, 4))[1] / 1000000.0, + 'lastmod'=>date('r', unpack('V', substr($payload, 144, 4))[1]) + ]; + + return $contact; + } + + public static function parseAppStart(string $payload) { + $info = [ + 'response_code'=>ord($payload[0]), + 'advert_type'=>ord($payload[1]), + 'tx_power_dbm'=>ord($payload[2]), + 'max_tx_power'=>ord($payload[3]), + 'public_key'=>base64_encode(substr($payload, 4, CoreProtocol::PUB_KEY_SIZE)), + 'lat'=>unpack('l', substr($payload, 36, 4))[1] / 1000000.0, + 'long'=>unpack('l', substr($payload, 40, 4))[1] / 1000000.0, + 'multi_acks'=>ord($payload[44]), + 'advert_loc_policy'=>ord($payload[45]), + ]; + + return $info; + } + + public static function parseMessage(string $payload) { + $data = [ + 'code'=>ord($payload[0]), + 'channel_idx'=>ord($payload[1]), + 'path_len'=>ord($payload[2]), + 'type'=>ord($payload[3]), + 'timestamp'=>date('r', unpack('V', substr($payload, 4, 4))[1]), + 'message'=>substr($payload, 8, strlen($payload)) + ]; + + return $data; + } +} \ No newline at end of file diff --git a/src/CoreProtocol.php b/src/CoreProtocol.php new file mode 100644 index 0000000..e955a78 --- /dev/null +++ b/src/CoreProtocol.php @@ -0,0 +1,232 @@ + 0 ) { + while (true) { + $b = self::read_exact($fp, 1); + if ($b === chr(0x3E)) { // radio -> app + break; + } + } + + $lenBytes = self::read_exact($fp, 2); + $len = unpack('v', $lenBytes)[1]; + + $result = self::read_exact($fp, $len); + if( Environment::getDebug() ) echo "Reading frame:\n" . \Menking\Meshcore\Util\Debug::hexDump($result) . "\n"; + return $result; + } + else { + return chr(0x01) . chr(0xff); + } + } + + /** + * + * @param mixed $fp + * @param int $n + * @return string + */ + private static function read_exact($fp, int $n): string { + $buf = ''; + while (strlen($buf) < $n) { + $chunk = fread($fp, $n - strlen($buf)); + if ($chunk === false || $chunk === '') { + usleep(10000); + continue; + } + $buf .= $chunk; + } + return $buf; + } + + /** + * Changes the serial port + * + * @param string $device + * @return bool + * @throws RuntimeException + */ + public static function configureTty(string $device): bool { + if( PHP_OS_FAMILY == 'Linux' ) { + $cmd = sprintf( + 'stty -F %s %d raw -echo -icanon min 0 time 10', + escapeshellarg($device), + 115200 + ); + + exec($cmd . ' 2>&1', $output, $code); + + if ($code !== 0) { + throw new \RuntimeException( + "stty failed: " . implode("\n", $output) + ); + } + else { + return true; + } + } + else { + throw new \RuntimeException("This library hasn't been tested on Windows or MacOS"); + } + } + + public static function getErrorText(int $error_code) { + switch($error_code) { + case self::ERR_CODE_UNSUPPORTED_CMD: + return "Unsupported command"; + case self::ERR_CODE_NOT_FOUND: + return "Not found"; + case self::ERR_CODE_TABLE_FULL: + return "Table full"; + case self::ERR_CODE_BAD_STATE: + return "Bad state"; + case self::ERR_CODE_FILE_IO_ERROR: + return "File I/O error"; + case self::ERR_CODE_ILLEGAL_ARG: + return "Illegal argument"; + case 0xff: + return "I/O timeout"; + default: + return "Unknown error: 0x" . dechex($error_code); + } + } + + public static function getLppType(int $type) { + switch($type) { + case self::LPP_TEMP: + return 'temperature'; + case self::LPP_VOLTAGE: + return 'voltage'; + case self::LPP_ILLUMINANCE: + return 'illuminance'; + case self::LPP_PRESENCE: + return 'presence'; + case self::LPP_HUMIDITY: + return 'humidity'; + case self::LPP_ACCEL: + return 'accelerometer'; + case self::LPP_BAROMETER: + return 'barometer'; + case self::LPP_GYRO: + return 'gyrometer'; + case self::LPP_GPS: + return 'gps'; + default: + return 'unknown'; + } + } + + const PUB_KEY_SIZE = 32; + + const CMD_APP_START = 1; + const CMD_SEND_TXT_MSG = 2; + const CMD_SEND_CHANNEL_TXT_MSG = 3; + const CMD_GET_CONTACTS = 4; + const CMD_GET_DEVICE_TIME = 5; + const CMD_SET_DEVICE_TIME = 6; + const CMD_SYNC_NEXT_MESSAGE = 10; + const CMD_SET_RADIO_PARAMS = 11; + const CMD_REBOOT = 19; + const CMD_GET_BATT_AND_STORAGE = 20; + const CMD_DEVICE_QUERY = 22; + const CMD_LOGIN = 26; + const CMD_SEND_STATUS_REQ = 27; + const CMD_HAS_CONNECTION = 28; + const CMD_LOGOUT = 29; + const CMD_GET_CHANNEL = 31; + const CMD_SEND_TELEMETRY_REQ = 39; + const CMD_SEND_BINARY_REQ = 50; // 0x32 + const CMD_SEND_ANON_REQ = 57; + + const RESP_CODE_OK = 0; + const RESP_CODE_ERR = 1; + const RESP_CODE_CONTACTS_START = 2; + const RESP_CODE_CONTACT = 3; + const RESP_CODE_END_OF_CONTACTS = 4; + const RESP_CODE_SELF_INFO = 5; + const RESP_CODE_SENT = 6; + const RESP_CODE_CURR_TIME = 9; + const RESP_CODE_BATT_AND_STORAGE = 12; + const RESP_CODE_DEVICE_INFO = 13; + + const ERR_CODE_UNSUPPORTED_CMD = 1; + const ERR_CODE_NOT_FOUND = 2; + const ERR_CODE_TABLE_FULL = 3; + const ERR_CODE_BAD_STATE = 4; + const ERR_CODE_FILE_IO_ERROR = 5; + const ERR_CODE_ILLEGAL_ARG = 6; + + // these are _pushed_ to client app at any time + const PUSH_CODE_ADVERT = 0x80; + const PUSH_CODE_PATH_UPDATED = 0x81; + const PUSH_CODE_SEND_CONFIRMED = 0x82; + const PUSH_CODE_MSG_WAITING = 0x83; + const PUSH_CODE_RAW_DATA = 0x84; + const PUSH_CODE_LOGIN_SUCCESS = 0x85; + const PUSH_CODE_LOGIN_FAIL = 0x86; + const PUSH_CODE_STATUS_RESPONSE = 0x87; + const PUSH_CODE_LOG_RX_DATA = 0x88; + const PUSH_CODE_TRACE_DATA = 0x89; + const PUSH_CODE_NEW_ADVERT = 0x8A; + const PUSH_CODE_TELEMETRY_RESPONSE = 0x8B; + const PUSH_CODE_BINARY_RESPONSE = 0x8C; + const PUSH_CODE_PATH_DISCOVERY_RESPONSE = 0x8D; + const PUSH_CODE_CONTROL_DATA = 0x8E; // v8+ + const PUSH_CODE_CONTACT_DELETED = 0x8F; // used to notify client app of deleted contact when overwriting oldest + const PUSH_CODE_CONTACTS_FULL = 0x90; // used to notify client app that contacts storage is full + + const BINREQ_STATUS = 0x01; + const BINREQ_KEEP_ALIVE = 0x02; + const BINREQ_TELEMETRY = 0x03; + const BINREQ_MMA = 0x04; + const BINREQ_ACL = 0x05; + const BINREQ_NEIGHBORS = 0x06; + + const ANONREQ_REGIONS = 0x01; + const ANONREQ_OWNER = 0x02; + const ANONREQ_BASIC = 0x03; + + const LPP_VOLTAGE = 0x74; + const LPP_TEMP = 0x67; + const LPP_ILLUMINANCE = 0x65; + const LPP_PRESENCE = 0x66; + const LPP_HUMIDITY = 0x68; + const LPP_ACCEL = 0x71; + const LPP_BAROMETER = 0x73; + const LPP_GYRO = 0x86; + const LPP_GPS = 0x88; + +} diff --git a/src/Environment.php b/src/Environment.php new file mode 100644 index 0000000..47cde99 --- /dev/null +++ b/src/Environment.php @@ -0,0 +1,20 @@ +serial = fopen(Environment::getDevice(), 'r+b'); + + if( !$this->serial ) { + throw new RuntimeException("Could not open " . Environment::getDevice()); + } + } + + public static function getInstance(): Meshcore { + if( self::$instance == null && Environment::getDevice() == null ) { + throw new RuntimeException("Please initialize Meshcore by calling Environment::configure()"); + } + else if( self::$instance == null ) { + self::$instance = new Meshcore(); + } + + return self::$instance; + } + + /** + * Initialize the connection. Needed for most commands to succeed. + * + * @return true|array{code: int, pub_key: string, type: int, flags: int, out_path_len: int, out_path: string, contact_name: string, last_advert_timestamp: string, gps_lat: int|float, gps_lon: int|float, lastmod: string}|array{code: int, sent_flood: int, pending_login: mixed, est_timeout: mixed}|string + */ + public function appStart(string $app_name) { + CoreProtocol::writeFrame($this->serial, chr(CoreProtocol::CMD_APP_START) . chr(0x00) . " " . $app_name); + $response = CoreProtocol::readFrame($this->serial); + + return CoreParser::parseResponse($response); + } + + /** + * + * @return BatteryStorageResponse + */ + public function getBatteryAndStorage(): BatteryStorageResponse { + CoreProtocol::writeFrame($this->serial, chr(CoreProtocol::CMD_GET_BATT_AND_STORAGE)); + $response = CoreProtocol::readFrame($this->serial); + + return CoreParser::parseResponse($response); + } + + /** + * + * @return void + */ + public function reboot() { + CoreProtocol::writeFrame($this->serial, chr(CoreProtocol::CMD_REBOOT) . "reboot"); + } + + /** + * + * @return DeviceInfoResponse + */ + public function getDeviceInfo(): DeviceInfoResponse { + CoreProtocol::writeFrame($this->serial, chr(CoreProtocol::CMD_DEVICE_QUERY) . 0x03); + $response = CoreProtocol::readFrame($this->serial); + + return CoreParser::parseResponse($response); + } + + /** + * + * @param int $channel_idx + * @return array{response_code: int, channel_idx: int, channel: string[], channel_secret: string} + */ + public function getChannel(int $channel_idx) { + CoreProtocol::writeFrame($this->serial, chr(CoreProtocol::CMD_GET_CHANNEL) . chr($channel_idx)); + $response = CoreProtocol::readFrame($this->serial); + + return CoreParser::parseResponse($response); + } + + public function setRadioParams(float $freq, float $bw, int $sf, int $cr) { + /* + $params = [ + 'freq'=>910.525, + 'bw'=>62.5, + 'sf'=>7, + 'cr'=>5, + ]; + */ + + $payload = chr(CoreProtocol::CMD_SET_RADIO_PARAMS) + . pack('V', $freq * 1000) + . pack('V', $bw * 1000) + . chr($sf) + . chr($cr) + ; + + CoreProtocol::writeFrame($this->serial, $payload); + $response = CoreProtocol::readFrame($this->serial); + + return CoreParser::parseResponse($response); + } + + public function getContacts(): array { + CoreProtocol::writeFrame($this->serial, chr(CoreProtocol::CMD_GET_CONTACTS)); + $response = CoreProtocol::readFrame($this->serial); + $contacts = []; + + if( ord($response[0]) == 0x02 ) { + do { + $response = CoreProtocol::readFrame($this->serial); + if( ord($response[0]) == 0x03 ) { + $contacts[] = CoreParser::getContact($response); + } + } + while(ord($response[0]) != 0x04); + } + + return $contacts; + } + + public function getNextMessage(?string $tag = null) { + CoreProtocol::writeFrame($this->serial, chr(CoreProtocol::CMD_SYNC_NEXT_MESSAGE)); + $response = CoreProtocol::readFrame($this->serial); + + if( ord($response[0]) == 0x0a ) { + return []; + } + + return CoreParser::parseResponse($response); + } + + public function sendChannelTxtMessage(string $message, int $channel_idx): bool { + $payload = chr(CoreProtocol::CMD_SEND_CHANNEL_TXT_MSG) . chr(0x00) . chr($channel_idx) . pack('V', time()) . "$message\0"; + + CoreProtocol::writeFrame($this->serial, $payload); + $response = CoreProtocol::readFrame($this->serial); + + return CoreParser::parseResponse($response); + } + + public function getDeviceTime(){ + CoreProtocol::writeFrame($this->serial, chr(CoreProtocol::CMD_GET_DEVICE_TIME)); + $response = CoreProtocol::readFrame($this->serial); + + return CoreParser::parseResponse($response); + } + + public function setDeviceTime() { + CoreProtocol::writeFrame($this->serial, chr(CoreProtocol::CMD_SET_DEVICE_TIME) . pack('V', time())); + $response = CoreProtocol::readFrame($this->serial); + + return CoreParser::parseResponse($response); + } + + /** + * + * @param string $contact_pk + * @param string $password + * @return true|array{code: int, pub_key: string, type: int, flags: int, out_path_len: int, out_path: string, contact_name: string, last_advert_timestamp: string, gps_lat: int|float, gps_lon: int|float, lastmod: string}|array{code: int, sent_flood: int, pending_login: mixed, est_timeout: mixed}|string + */ + public function login(string $contact_pk, string $password) { + $payload = chr(CoreProtocol::CMD_LOGIN) . str_pad(base64_decode($contact_pk), CoreProtocol::PUB_KEY_SIZE, "\0") . $password . chr(0x00); + + CoreProtocol::writeFrame($this->serial, $payload); + $response = CoreProtocol::readFrame($this->serial); + + do { + usleep(250000); + $response = CoreProtocol::readFrame($this->serial); + } + while(ord($response[0]) != 0x85 && ord($response[0]) != 0x86 && ord($response[0]) != 0x01); + + return CoreParser::parseResponse($response); + } + + /** + * + * @param string $contact_pk + * @return true|array{code: int, pub_key: string, type: int, flags: int, out_path_len: int, out_path: string, contact_name: string, last_advert_timestamp: string, gps_lat: int|float, gps_lon: int|float, lastmod: string}|array{code: int, sent_flood: int, pending_login: mixed, est_timeout: mixed}|string + */ + public function disconnect(string $contact_pk) { + CoreProtocol::writeFrame($this->serial, chr(CoreProtocol::CMD_LOGOUT) . base64_decode($contact_pk) . "\0"); + $response = CoreProtocol::readFrame($this->serial); + + return CoreParser::parseResponse($response); + } + + + public function statusRequest(string $contact_pk ) { + $payload = chr(CoreProtocol::CMD_SEND_STATUS_REQ) . base64_decode($contact_pk); + + CoreProtocol::writeFrame($this->serial, $payload); + $response = CoreProtocol::readFrame($this->serial); + + return CoreParser::parseResponse($response); + } + + public function connected(string $contact_pk): bool { + CoreProtocol::writeFrame($this->serial, chr(CoreProtocol::CMD_HAS_CONNECTION) . base64_decode($contact_pk) . "\0"); + $response = CoreProtocol::readFrame($this->serial); + + return ord($response[0]) != 0x01; + } + + public function getTelemetryRequest(string $contact_pk) { + CoreProtocol::writeFrame($this->serial, chr(CoreProtocol::CMD_SEND_TELEMETRY_REQ) . base64_decode($contact_pk)); + $response = CoreProtocol::readFrame($this->serial); + + return CoreParser::parseResponse($response); + } + + public function sendAnonRequest(string $contact_pk) { + CoreProtocol::writeFrame($this->serial, chr(CoreProtocol::CMD_SEND_ANON_REQ) . base64_decode($contact_pk) . "\0"); + $response = CoreProtocol::readFrame($this->serial); + + return CoreParser::parseResponse($response); + } + + public function sendBinaryRequest(string $request): BinaryResponse { + CoreProtocol::writeFrame($this->serial, chr(CoreProtocol::CMD_SEND_BINARY_REQ) . $request); + $response = CoreProtocol::readFrame($this->serial); + + return CoreParser::parseResponse($response); + } + + public function poll(int $timeout_ms = 5000): void { + $mark = time(); + + do { + $response = CoreProtocol::readFrame($this->serial); + echo json_encode(CoreParser::parseResponse($response)); + } + while( (time() - $mark) < $timeout_ms ) ; //ord($response[0]) == 0x01 && ord($response[1]) == 0xff); + } + +} \ No newline at end of file diff --git a/src/Model/BatteryStorageResponse.php b/src/Model/BatteryStorageResponse.php new file mode 100644 index 0000000..36d4f3a --- /dev/null +++ b/src/Model/BatteryStorageResponse.php @@ -0,0 +1,22 @@ += 32 && $byte <= 126) + ? chr($byte) + : ' '; + } + + // Pad last line so ASCII lines up + $hex = str_pad($hex, $width * 3); + + $output .= $hex . ' | ' . $ascii . PHP_EOL; + } + + return $output; + } +} \ No newline at end of file diff --git a/tests/getBatteryStorage.php b/tests/getBatteryStorage.php new file mode 100644 index 0000000..c6c1850 --- /dev/null +++ b/tests/getBatteryStorage.php @@ -0,0 +1,21 @@ +\n"); + +Environment::configure($argv[1]); + +$mc = Meshcore::getInstance(); + +$resp = $mc->appStart("battery storage"); + +$resp = $mc->getBatteryAndStorage(); + +echo "Battery voltage: " . $resp->battery_millivolt . " mV\n"; +echo "Storage used: " . $resp->storage_used . " kB\n"; +echo "Storage total: " . $resp->storage_total . " kB\n"; + diff --git a/tests/getContacts.php b/tests/getContacts.php new file mode 100644 index 0000000..edd88d3 --- /dev/null +++ b/tests/getContacts.php @@ -0,0 +1,18 @@ +"); + +Environment::configure($argv[1]); + +$mc = Meshcore::getInstance(); + +$mc->appStart("app_get_contacts"); + +$resp = $mc->getContacts(); + +print_r($resp); \ No newline at end of file diff --git a/tests/getDeviceInfo.php b/tests/getDeviceInfo.php new file mode 100644 index 0000000..419f09f --- /dev/null +++ b/tests/getDeviceInfo.php @@ -0,0 +1,19 @@ +\n"); + +Environment::configure($argv[1]); + +$mc = Meshcore::getInstance(); + +$mc->appStart("app_get_contacts"); + +$resp = $mc->getDeviceInfo(); + +print_r($resp); + diff --git a/tests/getDeviceTime.php b/tests/getDeviceTime.php new file mode 100644 index 0000000..af00f05 --- /dev/null +++ b/tests/getDeviceTime.php @@ -0,0 +1,18 @@ +\n"); + +Environment::configure($argv[1]); + +$mc = Meshcore::getInstance(); + +$mc->appStart("getdevicetime"); + +$ts = $mc->getDeviceTime(); + +echo "Current device time: " . date('r', $ts['current_time']) . "\n"; diff --git a/tests/getMessages.php b/tests/getMessages.php new file mode 100644 index 0000000..aa32b51 --- /dev/null +++ b/tests/getMessages.php @@ -0,0 +1,20 @@ +\n"); + +Environment::configure($argv[1]); + +$mc = Meshcore::getInstance(); + +$mc->appStart("get_messages"); + +$resp = $mc->getNextMessage(); + +print_r($resp); +exit; diff --git a/tests/getRemote.php b/tests/getRemote.php new file mode 100644 index 0000000..23953e8 --- /dev/null +++ b/tests/getRemote.php @@ -0,0 +1,44 @@ +\n"); + +Environment::configure($argv[1]); + +$mc = Meshcore::getInstance(); + +$x = $mc->appStart("get remote tele"); + +$contacts = $mc->getContacts(); +$idx = 1; +foreach($contacts as $contact) { + echo "(" . $idx++ . ") {$contact['contact_name']}\n"; +} + +$choice = readline("\nPlease select contact to attempt remote login: "); +$contact = $contacts[$choice - 1]; + +echo "Using {$contact['contact_name']}\n"; + +$password = readline("Please enter contact's password: "); + +$resp = $mc->login($contact['pub_key'], $password); + +if( !$resp['status'] ) die("Login failed\n"); + +$payload = + chr(0x00) + . chr(0xff) // count + . pack('v', 0x0000) // offset + . chr(0x00) + . chr(0x04) + . pack('V', time()); + +echo "Payload: \n" . \Menking\Meshcore\Util\Debug::hexDump($payload) . "\n"; +CoreProtocol::writeFrame($this->serial, $payload); +$response = CoreProtocol::readFrame($this->serial); diff --git a/tests/getStatusRequest.php b/tests/getStatusRequest.php new file mode 100644 index 0000000..be13ab3 --- /dev/null +++ b/tests/getStatusRequest.php @@ -0,0 +1,33 @@ +\n"); + +Environment::configure($argv[1]); + +$mc = Meshcore::getInstance(); + +$mc->appStart("getstatusreq"); + +$contacts = $mc->getContacts(); +$idx = 1; +foreach($contacts as $contact) { + echo "(" . $idx++ . ") {$contact['contact_name']}\n"; +} + +$choice = readline("\nPlease select contact to attempt remote login: "); +$contact = $contacts[$choice - 1]; + +echo "Using {$contact['contact_name']}\n"; + +print_r($mc->login($contact['pub_key'], '')); + +$ts = $mc->statusRequest($contact['pub_key']); + +print_r($ts); + +$mc->poll(); diff --git a/tests/getTelemetry.php b/tests/getTelemetry.php new file mode 100644 index 0000000..fc2e1bf --- /dev/null +++ b/tests/getTelemetry.php @@ -0,0 +1,55 @@ +\n"); + +Environment::configure($argv[1]); + +$mc = Meshcore::getInstance(); + +$x = $mc->appStart("get remote tele"); + +$contacts = $mc->getContacts(); +$idx = 1; +foreach($contacts as $contact) { + echo "(" . $idx++ . ") {$contact['contact_name']} [" . substr($contact['pub_key'], 0, 6) . "]\n"; +} + +$choice = readline("\nPlease select contact to attempt remote login: "); +$contact = $contacts[$choice - 1]; + +echo "Using {$contact['contact_name']}\n"; + +/* +if( !$mc->connected($contact['pub_key']) ) { + $password = readline("Password: "); + + $mc->login($contact['pub_key'], $password); +} + */ + +$req = base64_decode($contact['pub_key']) . chr(CoreProtocol::BINREQ_TELEMETRY); + +$result = $mc->sendBinaryRequest($req); +echo "Tag: {$result['tag']}\n"; + +$timeout = 0; + +while($timeout++ < 6) { + echo "timeout: $timeout\n"; + $resp = $mc->getSyncNextMessage(); + if( !empty($resp) ) { + print_r($resp); + if( $resp['code'] == 0x8c) { + exit; + } + } + sleep(1); +} + +echo "Complete\n"; diff --git a/tests/reboot.php b/tests/reboot.php new file mode 100644 index 0000000..dfe0935 --- /dev/null +++ b/tests/reboot.php @@ -0,0 +1,15 @@ +\n"); + +Environment::configure($argv[1]); + +$mc = Meshcore::getInstance(); + +$mc->appStart("reboot"); +$mc->reboot(); diff --git a/tests/sendMessage.php b/tests/sendMessage.php new file mode 100644 index 0000000..46b2aad --- /dev/null +++ b/tests/sendMessage.php @@ -0,0 +1,17 @@ +appStart("send message"); +$resp = $mc->sendChannelTxtMessage("Ping", 0); // default channel Public on 0 + +echo Debug::hexDump($resp); +exit; diff --git a/tests/setDeviceTime.php b/tests/setDeviceTime.php new file mode 100644 index 0000000..c0d3ab0 --- /dev/null +++ b/tests/setDeviceTime.php @@ -0,0 +1,21 @@ +\n"); + +Environment::configure($argv[1]); + +$mc = Meshcore::getInstance(); + +$mc->appStart("set device"); + +echo "Setting device time\n"; +$result = $mc->setDeviceTime(); + +print_r($result); + +$mc->poll(); \ No newline at end of file