Files
meshcore-php/src/CoreParser.php
T
2026-06-04 10:25:41 -04:00

494 lines
17 KiB
PHP

<?php
namespace Menking\Meshcore;
use Exception;
use Menking\Meshcore\Model\AdvertPathResponse;
use Menking\Meshcore\Model\AdvertResponse;
use Menking\Meshcore\Model\AppStartResponse;
use Menking\Meshcore\Model\BinaryResponse;
use Menking\Meshcore\Model\ChannelResponse;
use Menking\Meshcore\Model\CodeSentResponse;
use Menking\Meshcore\Model\CurrentTimeResponse;
use Menking\Meshcore\Model\DeviceInfoResponse;
use Menking\Meshcore\Model\ErrorResponse;
use Menking\Meshcore\Model\LoginResponse;
use Menking\Meshcore\Model\LogReceiveResponse;
use Menking\Meshcore\Model\BatteryStorageResponse;
use Menking\Meshcore\Model\CodeStatusResponse;
use Menking\Meshcore\Model\ContactResponse;
use Menking\Meshcore\Model\ContactsFullResponse;
use Menking\Meshcore\Model\MessageResponse;
use Menking\Meshcore\Model\MessagesWaitingResponse;
use Menking\Meshcore\Model\NoMoreMessagesResponse;
use Menking\Meshcore\Model\OkResponse;
class CoreParser {
/**
*
* @param string $payload
* @throws Exception
*/
public static function parseResponse(string $payload): mixed {
switch(ord($payload[0])) {
case CoreProtocol::RESP_CODE_OK:
return self::parseOk($payload);
case CoreProtocol::RESP_CODE_ERR:
//throw new \Exception("Protocol error: " . CoreProtocol::getErrorText(ord($payload[1])));
return self::parseError($payload);
case CoreProtocol::RESP_CODE_CONTACT:
return self::getContact($payload);
case CoreProtocol::RESP_CODE_SELF_INFO:
return self::parseAppStart($payload);
case CoreProtocol::RESP_CODE_SENT:
return self::parseCodeSent($payload);
case CoreProtocol::PUSH_CODE_LOGIN_SUCCESS:
return self::parseLoginSuccess($payload);
case CoreProtocol::PUSH_CODE_LOG_RX_DATA:
return self::parseLogRxData($payload);
case CoreProtocol::PUSH_CODE_LOGIN_FAIL:
return self::parseLoginFail($payload);
case CoreProtocol::RESP_CODE_CURR_TIME:
return self::parseCurrentTime($payload);
case CoreProtocol::PUSH_CODE_BINARY_RESPONSE:
return self::parseBinaryResponse($payload);
case CoreProtocol::RESP_CODE_BATT_AND_STORAGE:
return self::parseBatteryAndStorage($payload);
case CoreProtocol::RESP_CODE_NO_MORE_MESSAGES:
return new NoMoreMessagesResponse();
case CoreProtocol::RESP_CODE_ADVERT_PATH:
return self::parseAdvertPathResponse($payload);
case CoreProtocol::RESP_CODE_STATS:
return self::parseCodeStatusResponse($payload);
case CoreProtocol::RESP_CODE_DEVICE_INFO:
return self::parseDeviceInfoResponse($payload);
case CoreProtocol::RESP_CODE_CHANNEL_INFO:
return self::parseChannelInfoResponse($payload);
case CoreProtocol::PUSH_CODE_MSG_WAITING:
return new MessagesWaitingResponse();
case CoreProtocol::RESP_CODE_CONTACT_MSG_RECV_V3:
case CoreProtocol::RESP_CODE_CHANNEL_MSG_RECV_V3:
case CoreProtocol::RESP_CODE_CHANNEL_MSG_RECV:
return self::parseMsg($payload);
case CoreProtocol::PUSH_CODE_ADVERT:
case CoreProtocol::PUSH_CODE_NEW_ADVERT:
return self::parseAdvert($payload);
case CoreProtocol::PUSH_CODE_CONTACTS_FULL:
return new ContactsFullResponse();
default:
echo "Unparsed response: " . \Menking\Meshcore\Util\Debug::hexDump($payload) . "\n";
return $payload;
}
}
protected static function parseOk(string $payload): OkResponse {
$m = new OkResponse();
$m->code = ord($payload[0]);
return $m;
}
protected static function parseMsg(string $payload): mixed {
$m = new MessageResponse();
$idx = 0;
$m->code = ord($payload[$idx]);
$idx++;
$m->snr = 0;
if( $m->code == CoreProtocol::RESP_CODE_CHANNEL_MSG_RECV_V3 ) {
$m->snr = ord($payload) / 4;
$idx += 1;
// two reserved bytes
$idx += 2;
}
$m->channel_idx = ord($payload[$idx]);
$m->path_len = ord($payload[$idx+1]);
$m->type = ord($payload[$idx+2]);
$m->timestamp = unpack('V', substr($payload, $idx+3, 4))[1];
$m->message = substr($payload, $idx+7);
return $m;
}
/**
*
* @param string $payload
* @return mixed
*/
protected static function parseAdvert(string $payload): mixed {
$m = new AdvertResponse();
$m->code = ord($payload[0]);
$m->pub_key = self::formatPublicKey(substr($payload, 1, CoreProtocol::PUB_KEY_SIZE));
if( strlen($payload) > CoreProtocol::PUB_KEY_SIZE + 2 ) {
$m->type = ord($payload[33]);
$m->flags = ord($payload[34]);
$m->out_path = self::formatPublicKey(substr($payload, 35, CoreProtocol::MAX_PATH_SIZE+1));
$m->name = rtrim(substr($payload, 100, 32));
if( strlen($payload) > 132 ) {
$m->last_advert_time = unpack('V', substr($payload, 132, 4))[1];
$m->lat = unpack('V', substr($payload, 136, 4))[1];
$m->lon = unpack('V', substr($payload, 140, 4))[1];
$m->last_mod = unpack('V', substr($payload, 144, 4))[1];
}
}
return $m;
}
/**
*
* @param string $payload
* @return BinaryResponse
*/
protected static function parseBinaryResponse(string $payload): BinaryResponse {
$data = new BinaryResponse();
$data->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 = self::formatPublicKey(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 = self::formatPublicKey(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]) >= 128) ? (ord($payload[2]) - 256): ord($payload[2]);
//$data->flags = ord($payload[1]);
//$data->snr = ord($payload[2]) / 4;
$data->raw = 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 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;
}
/**
*
* @param string $payload
* @return ContactResponse
*/
public static function getContact(string $payload): ContactResponse {
$m = new ContactResponse();
$m->code = ord($payload[0]);
$m->pub_key = self::formatPublicKey(substr($payload, 1, CoreProtocol::PUB_KEY_SIZE));
$m->type = ord($payload[33]);
$m->flags = ord($payload[34]);
$m->out_path_len = ord($payload[35]);
$m->out_path = self::formatPublicKey(substr($payload, 36, 64));
$m->name = trim(substr($payload, 100, 32), ' ');
$m->last_advert_timestamp = date('r', unpack('V', substr($payload, 132, 4))[1]);
$m->lat = unpack('l', substr($payload, 136, 4))[1] / 1000000.0;
$m->lon = unpack('l', substr($payload, 140, 4))[1] / 1000000.0;
$m->lastmod = date('r', unpack('V', substr($payload, 144, 4))[1]);
return $m;
}
/**
*
* @param string $payload
* @return AppStartResponse
*/
protected static function parseAppStart(string $payload): AppStartResponse {
$m = new AppStartResponse();
$m->code = ord($payload[0]);
$m->advert_type = ord($payload[1]);
$m->tx_power_dbm = ord($payload[2]);
$m->max_tx_power = ord($payload[3]);
$m->pub_key = self::formatPublicKey(substr($payload, 4, CoreProtocol::PUB_KEY_SIZE));
$m->lat = unpack('l', substr($payload, 36, 4))[1] / 1000000.0;
$m->lon = unpack('l', substr($payload, 40, 4))[1] / 1000000.0;
$m->multi_acks = ord($payload[44]);
$m->advert_loc_policy = ord($payload[45]);
return $m;
}
/**
*
* @param string $payload
* @return AdvertPathResponse
*/
protected static function parseAdvertPathResponse(string $payload): AdvertPathResponse {
$m = new AdvertPathResponse();
$m->code = ord($payload[0]);
$m->received_timestamp = unpack('V', substr($payload, 1, 4))[1];
$m->path_len = ord($payload[5]);
$m->path = substr($payload, 6, ord($payload[5]));
return $m;
}
/**
*
* @param string $payload
* @return CodeStatusTypeCoreResponse|CodeStatusTypeRadioResponse|CodeStatusTypePacketResponse
*/
protected static function parseCodeStatusResponse(string $payload): CodeStatusResponse {
$m = null;
switch(ord($payload[1])) {
case CoreProtocol::STATS_TYPE_CORE:
$m = new CodeStatusResponse();
$m->code = ord($payload[0]);
$m->type = CodeStatusResponse::RESP_CORE;
$m->battery_mv = unpack('v', substr($payload, 2, 2))[1];
$m->uptime_secs = unpack('V', substr($payload, 4, 4))[1];
$m->err_flags = unpack('v', substr($payload, 8, 2))[1];
$m->queue_len = ord($payload[10]);
break;
case CoreProtocol::STATS_TYPE_RADIO:
$m = new CodeStatusResponse();
$m->code = ord($payload[0]);
$m->type = CodeStatusResponse::RESP_RADIO;
$m->noise_floor = unpack('v', substr($payload, 2, 2))[1];
$m->last_rssi = (ord($payload[4]) >= 128) ? (ord($payload[4]) - 256): ord($payload[4]);
$m->last_snr = ord($payload[5]);;
$m->tx_air_secs = unpack('V', substr($payload, 6, 4))[1];
$m->rx_air_secs = unpack('V', substr($payload, 10, 4))[1];
break;
case CoreProtocol::STATS_TYPE_PACKETS:
$m = new CodeStatusResponse();
$m->code = ord($payload[0]);
$m->type = CodeStatusResponse::RESP_PACKET;
$m->recv = unpack('V', substr($payload, 2, 4))[1];
$m->sent = unpack('V', substr($payload, 6, 4))[1];
$m->n_sent_flood = unpack('V', substr($payload, 10, 4))[1];
$m->n_sent_direct = unpack('V', substr($payload, 14, 4))[1];
$m->n_recv_flood = unpack('V', substr($payload, 18, 4))[1];
$m->n_recv_direct = unpack('V', substr($payload, 22, 4))[1];
$m->n_recv_errors = unpack('V', substr($payload, 26, 4))[1];
break;
}
return $m;
}
/**
*
* @param string $payload
* @return DeviceInfoResponse
*/
protected static function parseDeviceInfoResponse(string $payload): DeviceInfoResponse {
$m = new DeviceInfoResponse();
$m->code = ord($payload[0]);
$m->firmware_version = ord($payload[1]);
$m->max_contacts_raw = ord($payload[2]) * 2;
$m->max_channels = ord($payload[3]);
$m->ble_pin = unpack('V', substr($payload, 4, 4))[1];
$m->firmware_build = trim(substr($payload, 8, 12));
$m->model = trim(substr($payload, 20, 40));
$m->version =trim(substr($payload, 60, 20));
$m->client_repeat_enabled = ord($payload[80]);
$m->path_hash_mode = ord($payload[81]);
return $m;
}
/**
*
* @param string $payload
* @return ChannelResponse
*/
protected static function parseChannelInfoResponse(string $payload): ChannelResponse {
$m = new ChannelResponse();
$m->code = ord($payload[0]);
$m->channel_idx = ord($payload[1]);
$m->channel_name = self::readCstring(substr($payload, 2, 32));
$m->channel_secret = substr($payload, 34, 16);
return $m;
}
/**
*
* @param mixed $str
* @return null|string
*/
public static function readCstring($str) {
$x = strpos($str, "\0");
if( $x === false ) {
return null;
}
else {
return trim(substr($str, 0, $x));
}
}
/**
*
* @param string $pubkey
* @return string
*/
public static function formatPublicKey(string $pubkey): string {
return bin2hex($pubkey);
}
}