bug: fix some issues with whatsminer BTMinerV3 implementation

This commit is contained in:
Brett Rowan
2025-08-16 09:14:11 -06:00
parent 27bb06de2b
commit e1a9cc5d19
2 changed files with 108 additions and 66 deletions

View File

@@ -783,67 +783,67 @@ class BTMinerV2(StockFirmware):
BTMINERV3_DATA_LOC = DataLocations( BTMINERV3_DATA_LOC = DataLocations(
**{ **{
str(DataOptions.MAC): DataFunction( str(DataOptions.MAC): DataFunction(
"_get_mac", [RPCAPICommand("rpc_get_device_info", "get_device_info")] "_get_mac", [RPCAPICommand("rpc_get_device_info", "get.device.info")]
), ),
str(DataOptions.API_VERSION): DataFunction( str(DataOptions.API_VERSION): DataFunction(
"_get_api_version", "_get_api_version",
[RPCAPICommand("rpc_get_device_info", "get_device_info")], [RPCAPICommand("rpc_get_device_info", "get.device.info")],
), ),
str(DataOptions.FW_VERSION): DataFunction( str(DataOptions.FW_VERSION): DataFunction(
"_get_firmware_version", "_get_firmware_version",
[RPCAPICommand("rpc_get_device_info", "get_device_info")], [RPCAPICommand("rpc_get_device_info", "get.device.info")],
), ),
str(DataOptions.HOSTNAME): DataFunction( str(DataOptions.HOSTNAME): DataFunction(
"_get_hostname", [RPCAPICommand("rpc_get_device_info", "get_device_info")] "_get_hostname", [RPCAPICommand("rpc_get_device_info", "get.device.info")]
), ),
str(DataOptions.FAULT_LIGHT): DataFunction( str(DataOptions.FAULT_LIGHT): DataFunction(
"_get_light_flashing", "_get_light_flashing",
[RPCAPICommand("rpc_get_device_info", "get_device_info")], [RPCAPICommand("rpc_get_device_info", "get.device.info")],
), ),
str(DataOptions.WATTAGE_LIMIT): DataFunction( str(DataOptions.WATTAGE_LIMIT): DataFunction(
"_get_wattage_limit", "_get_wattage_limit",
[RPCAPICommand("rpc_get_device_info", "get_device_info")], [RPCAPICommand("rpc_get_device_info", "get.device.info")],
), ),
str(DataOptions.FANS): DataFunction( str(DataOptions.FANS): DataFunction(
"_get_fans", "_get_fans",
[RPCAPICommand("rpc_get_miner_status_summary", "get_miner_status_summary")], [RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")],
), ),
str(DataOptions.FAN_PSU): DataFunction( str(DataOptions.FAN_PSU): DataFunction(
"_get_psu_fans", [RPCAPICommand("rpc_get_device_info", "get_device_info")] "_get_psu_fans", [RPCAPICommand("rpc_get_device_info", "get.device.info")]
), ),
str(DataOptions.HASHBOARDS): DataFunction( str(DataOptions.HASHBOARDS): DataFunction(
"_get_hashboards", "_get_hashboards",
[ [
RPCAPICommand("rpc_get_device_info", "get_device_info"), RPCAPICommand("rpc_get_device_info", "get.device.info"),
RPCAPICommand( RPCAPICommand(
"rpc_get_miner_status_edevs", "rpc_get_miner_status_edevs",
"get_miner_status_edevs", "get.miner.status:edevs",
), ),
], ],
), ),
str(DataOptions.POOLS): DataFunction( str(DataOptions.POOLS): DataFunction(
"_get_pools", "_get_pools",
[RPCAPICommand("rpc_get_miner_status_summary", "get_miner_status_summary")], [RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")],
), ),
str(DataOptions.UPTIME): DataFunction( str(DataOptions.UPTIME): DataFunction(
"_get_uptime", "_get_uptime",
[RPCAPICommand("rpc_get_miner_status_summary", "get_miner_status_summary")], [RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")],
), ),
str(DataOptions.WATTAGE): DataFunction( str(DataOptions.WATTAGE): DataFunction(
"_get_wattage", "_get_wattage",
[RPCAPICommand("rpc_get_miner_status_summary", "get_miner_status_summary")], [RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")],
), ),
str(DataOptions.HASHRATE): DataFunction( str(DataOptions.HASHRATE): DataFunction(
"_get_hashrate", "_get_hashrate",
[RPCAPICommand("rpc_get_miner_status_summary", "get_miner_status_summary")], [RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")],
), ),
str(DataOptions.EXPECTED_HASHRATE): DataFunction( str(DataOptions.EXPECTED_HASHRATE): DataFunction(
"_get_expected_hashrate", "_get_expected_hashrate",
[RPCAPICommand("rpc_get_miner_status_summary", "get_miner_status_summary")], [RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")],
), ),
str(DataOptions.ENVIRONMENT_TEMP): DataFunction( str(DataOptions.ENVIRONMENT_TEMP): DataFunction(
"_get_env_temp", "_get_env_temp",
[RPCAPICommand("rpc_get_miner_status_summary", "get_miner_status_summary")], [RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")],
), ),
} }
) )
@@ -1082,7 +1082,9 @@ class BTMinerV3(StockFirmware):
temp=board_data.get("chip-temp-min"), temp=board_data.get("chip-temp-min"),
inlet_temp=board_data.get("chip-temp-min"), inlet_temp=board_data.get("chip-temp-min"),
outlet_temp=board_data.get("chip-temp-max"), outlet_temp=board_data.get("chip-temp-max"),
serial_number=board_data.get(f"pcbsn{idx}"), serial_number=rpc_get_device_info.get("msg", {})
.get("miner", {})
.get(f"pcbsn{idx}"),
chips=board_data.get("effective-chips"), chips=board_data.get("effective-chips"),
expected_chips=self.expected_chips, expected_chips=self.expected_chips,
active=(board_data.get("hash-average") or 0) > 0, active=(board_data.get("hash-average") or 0) > 0,
@@ -1164,12 +1166,16 @@ class BTMinerV3(StockFirmware):
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary() rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
except APIError: except APIError:
return None return None
return ( res = (
rpc_get_miner_status_summary.get("msg", {}) rpc_get_miner_status_summary.get("msg", {})
.get("summary", {}) .get("summary", {})
.get("factory-hash") .get("factory-hash")
) )
if res == (-0.001 * self.expected_hashboards):
return None
return res
async def _get_env_temp( async def _get_env_temp(
self, rpc_get_miner_status_summary: dict = None self, rpc_get_miner_status_summary: dict = None
) -> float | None: ) -> float | None:

View File

@@ -23,6 +23,7 @@ import json
import logging import logging
import re import re
import struct import struct
import warnings
from asyncio import Future, StreamReader, StreamWriter from asyncio import Future, StreamReader, StreamWriter
from typing import Any, AsyncGenerator, Callable, Literal, Union from typing import Any, AsyncGenerator, Callable, Literal, Union
@@ -31,7 +32,7 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from passlib.handlers.md5_crypt import md5_crypt from passlib.handlers.md5_crypt import md5_crypt
from pyasic import settings from pyasic import settings
from pyasic.errors import APIError from pyasic.errors import APIError, APIWarning
from pyasic.misc import api_min_version, validate_command_output from pyasic.misc import api_min_version, validate_command_output
from pyasic.rpc.base import BaseMinerRPCAPI from pyasic.rpc.base import BaseMinerRPCAPI
@@ -1107,39 +1108,27 @@ class BTMinerV3RPCAPI(BaseMinerRPCAPI):
def __init__(self, ip: str, port: int = 4433, api_ver: str = "0.0.0"): def __init__(self, ip: str, port: int = 4433, api_ver: str = "0.0.0"):
super().__init__(ip, port, api_ver=api_ver) super().__init__(ip, port, api_ver=api_ver)
self.reader: StreamReader | None = None
self.writer: StreamWriter | None = None
self.reader_loop = None
self.salt = None self.salt = None
self.cmd_results = {} async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
self.cmd_callbacks = {"get.miner.report": set()} """Creates and sends multiple commands as one command to the miner.
async def connect(self): Parameters:
self.reader, self.writer = await asyncio.open_connection( *commands: The commands to send as a multicommand to the miner.
str(self.ip), self.port allow_warning: A boolean to supress APIWarnings.
)
self.reader_loop = asyncio.create_task(self._read_loop())
async def disconnect(self): """
self.writer.close() commands = self._check_commands(*commands)
await self.writer.wait_closed() data = await self._send_split_multicommand(*commands)
self.reader_loop.cancel() data["multicommand"] = True
return data
async def send_command( async def send_command(
self, command: str, parameters: Any = None, **kwargs self, command: str, parameters: Any = None, **kwargs
) -> dict: ) -> dict:
if self.writer is None: if ":" in command:
await self.connect() parameters = command.split(":")[1]
command = command.split(":")[0]
while command in self.cmd_results:
wait_fut = self.cmd_results[command]
await wait_fut
result_fut = Future()
self.cmd_results[command] = result_fut
cmd = {"cmd": command} cmd = {"cmd": command}
if parameters is not None: if parameters is not None:
cmd["param"] = parameters cmd["param"] = parameters
@@ -1159,33 +1148,80 @@ class BTMinerV3RPCAPI(BaseMinerRPCAPI):
# send the command # send the command
ser = json.dumps(cmd).encode("utf-8") ser = json.dumps(cmd).encode("utf-8")
header = struct.pack("<I", len(ser)) header = struct.pack("<I", len(ser))
await self._send_bytes(header + json.dumps(cmd).encode("utf-8")) return json.loads(
await self._send_bytes(header + json.dumps(cmd).encode("utf-8"))
)
await result_fut async def _send_bytes(
return result_fut.result() self,
data: bytes,
*,
port: int = None,
timeout: int = 100,
) -> bytes:
if port is None:
port = self.port
logging.debug(f"{self} - ([Hidden] Send Bytes) - Sending")
try:
# get reader and writer streams
reader, writer = await asyncio.open_connection(str(self.ip), port)
# handle OSError 121
except OSError as e:
if e.errno == 121:
logging.warning(
f"{self} - ([Hidden] Send Bytes) - Semaphore timeout expired."
)
return b"{}"
async def _read_loop(self): # send the command
while True: try:
result = await self._read_bytes() data_task = asyncio.create_task(self._read_bytes(reader, timeout=timeout))
data = self._load_api_data(result) logging.debug(f"{self} - ([Hidden] Send Bytes) - Writing")
command = data["desc"] writer.write(data)
if command in self.cmd_callbacks: logging.debug(f"{self} - ([Hidden] Send Bytes) - Draining")
callbacks: list[Callable] = self.cmd_callbacks[command] await writer.drain()
await asyncio.gather(*[callback(data) for callback in callbacks])
elif command in self.cmd_results: await data_task
future: Future = self.cmd_results.pop(command) ret_data = data_task.result()
future.set_result(data) except TimeoutError:
logging.warning(f"{self} - ([Hidden] Send Bytes) - Read timeout expired.")
return b"{}"
# close the connection
logging.debug(f"{self} - ([Hidden] Send Bytes) - Closing")
writer.close()
await writer.wait_closed()
return ret_data
def _check_commands(self, *commands) -> list:
return_commands = []
for command in commands:
if command.startswith("get.") or command.startswith("set."):
return_commands.append(command)
else: else:
logging.error(f"Received unexpected data for {self}: {data}") warnings.warn(
f"""Removing incorrect command: {command}
If you are sure you want to use this command please use API.send_command("{command}", ignore_errors=True) instead.""",
APIWarning,
)
return return_commands
async def _read_bytes(self, **kwargs) -> bytes: async def _read_bytes(self, reader: asyncio.StreamReader, timeout: int) -> bytes:
header = await self.reader.readexactly(4) ret_data = b""
length = struct.unpack("<I", header)[0]
return await self.reader.readexactly(length)
async def _send_bytes(self, data: bytes, **kwargs): # loop to receive all the data
self.writer.write(data) logging.debug(f"{self} - ([Hidden] Send Bytes) - Receiving")
await self.writer.drain() try:
header = await reader.readexactly(4)
length = struct.unpack("<I", header)[0]
ret_data = await reader.readexactly(length)
except (asyncio.CancelledError, asyncio.TimeoutError) as e:
raise e
except Exception as e:
logging.warning(f"{self} - ([Hidden] Send Bytes) - API Command Error {e}")
return ret_data
async def get_salt(self) -> str: async def get_salt(self) -> str:
if self.salt is not None: if self.salt is not None: