Goldshell Mini Doge Support (#375)

* Docs for Mini Doge

* Mini Doge Recognition Support

* Loading fixes

* Fix for number of fans

* Fixed expected_hashrate

* Fixes for hashboards

* Implemented uptime

* Fixed uptime

* Doc fixes

* Fixes for byte

* Copyright update

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Typo fix

* File renames

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Ryan Heideman
2025-08-26 20:06:10 -07:00
committed by GitHub
parent 038208efa6
commit 24134a5991
19 changed files with 340 additions and 29 deletions

View File

@@ -3,8 +3,8 @@
## Byte (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets

View File

@@ -0,0 +1,16 @@
# pyasic
## Mini Doge Models
## Mini Doge (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.goldshell.bfgminer.MiniDoge.MiniDoge.GoldshellMiniDoge
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -603,6 +603,12 @@ details {
<details>
<summary>Stock Firmware Goldshells:</summary>
<ul>
<details>
<summary>Mini Doge Series:</summary>
<ul>
<li><a href="../goldshell/MiniDoge#mini-doge-stock">Mini Doge (Stock)</a></li>
</ul>
</details>
<details>
<summary>X5 Series:</summary>
<ul>

View File

@@ -98,6 +98,7 @@ nav:
- Innosilicon A10X: "miners/innosilicon/A10X.md"
- Innosilicon A11X: "miners/innosilicon/A11X.md"
- Goldshell Byte: "miners/goldshell/Byte.md"
- Goldshell Mini Doge: "miners/goldshell/MiniDoge.md"
- Goldshell X5: "miners/goldshell/X5.md"
- Goldshell XMax: "miners/goldshell/XMax.md"
- Goldshell XBox: "miners/goldshell/XBox.md"

View File

@@ -254,6 +254,11 @@ class MinerConfig(BaseModel):
"""Constructs a MinerConfig object from web configuration for Goldshell miners."""
return cls(pools=PoolConfig.from_am_modern(web_conf))
@classmethod
def from_goldshell_list(cls, web_conf: list) -> "MinerConfig":
"""Constructs a MinerConfig object from web configuration for Goldshell miners."""
return cls(pools=PoolConfig.from_goldshell(web_conf))
@classmethod
def from_goldshell_byte(cls, web_conf: dict) -> "MinerConfig":
"""Constructs a MinerConfig object from web configuration for Goldshell Byte miners."""

View File

@@ -480,6 +480,7 @@ class GoldshellModels(MinerModelType):
KDBoxII = "KD Box II"
KDBoxPro = "KD Box Pro"
Byte = "Byte"
MiniDoge = "Mini Doge"
def __str__(self):
return self.value

View File

@@ -216,7 +216,7 @@ class BFGMiner(StockFirmware):
)
except LookupError:
pass
fans = [Fan(speed=d) if d else Fan() for d in fans_data]
fans = [Fan(speed=d) for d in fans_data if d is not None]
return fans

View File

@@ -1,5 +1,5 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# Copyright 2025 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -

View File

@@ -1,5 +1,5 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# Copyright 2025 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
@@ -13,4 +13,4 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .Byte import Byte
from .byte import Byte

View File

@@ -13,7 +13,8 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .Byte import *
from .byte import *
from .mini_doge import *
from .X5 import *
from .XBox import *
from .XMax import *

View File

@@ -0,0 +1,16 @@
# ------------------------------------------------------------------------------
# Copyright 2025 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .mini_doge import MiniDoge

View File

@@ -0,0 +1,27 @@
# ------------------------------------------------------------------------------
# Copyright 2025 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.device.algorithm import MinerAlgo
from pyasic.device.models import MinerModel
from pyasic.miners.device.makes import GoldshellMake
class MiniDoge(GoldshellMake):
raw_model = MinerModel.GOLDSHELL.MiniDoge
expected_chips = 40
expected_fans = 2
expected_hashboards = 1
algo = MinerAlgo.SCRYPT

View File

@@ -533,6 +533,7 @@ MINER_CLASSES = {
"GOLDSHELL KDBOXII": GoldshellKDBoxII,
"GOLDSHELL KDBOXPRO": GoldshellKDBoxPro,
"GOLDSHELL BYTE": GoldshellByte,
"GOLDSHELL MINIDOGE": GoldshellMiniDoge,
},
MinerTypes.BRAIINS_OS: {
None: BOSMiner,

View File

@@ -1,5 +1,5 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# Copyright 2025 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
@@ -73,14 +73,23 @@ GOLDSHELL_BYTE_DATA_LOC = DataLocations(
"_get_pools",
[RPCAPICommand("rpc_pools", "pools")],
),
str(DataOptions.UPTIME): DataFunction(
"_get_uptime",
[WebAPICommand("web_devs", "devs")],
),
str(DataOptions.WATTAGE): DataFunction(
"_get_wattage",
[WebAPICommand("web_devs", "devs")],
),
}
)
class GoldshellByte(GoldshellMiner, Byte):
data_locations = GOLDSHELL_BYTE_DATA_LOC
cgdev: dict | None = None
supports_shutdown = False
supports_power_modes = False
web_devs: dict | None = None
async def get_data(
self,
@@ -88,27 +97,22 @@ class GoldshellByte(GoldshellMiner, Byte):
include: List[Union[str, DataOptions]] = None,
exclude: List[Union[str, DataOptions]] = None,
) -> MinerData:
if self.cgdev is None:
if self.web_devs is None:
try:
self.cgdev = await self.web.send_command("cgminer?cgminercmd=devs")
self.web_devs = await self.web.devs()
except APIError:
pass
scrypt_board_count = 0
zksnark_board_count = 0
total_wattage = 0
total_uptime_mins = 0
for minfo in self.cgdev.get("minfos", []):
for minfo in self.web_devs.get("minfos", []):
algo_name = minfo.get("name")
for info in minfo.get("infos", []):
for _ in minfo.get("infos", []):
self.expected_hashboards += 1
self.expected_fans += 1
total_wattage = int(float(info.get("power", 0)))
total_uptime_mins = int(info.get("time", 0))
if algo_name == ALGORITHM_SCRYPT_NAME:
scrypt_board_count += 1
elif algo_name == ALGORITHM_ZKSNARK_NAME:
@@ -125,12 +129,6 @@ class GoldshellByte(GoldshellMiner, Byte):
data = await super().get_data(allow_warning, include, exclude)
data.expected_chips = self.expected_chips
data.wattage = total_wattage
data.uptime = total_uptime_mins
data.voltage = 0
for board in data.hashboards:
data.voltage += board.voltage
return data
@@ -313,3 +311,39 @@ class GoldshellByte(GoldshellMiner, Byte):
fans = [Fan(speed=d) if d else Fan() for d in fans_data]
return fans
async def _get_uptime(self, web_devs: dict = None) -> Optional[int]:
if web_devs is None:
try:
web_devs = await self.web.devs()
except APIError:
pass
if web_devs is not None:
try:
for minfo in self.web_devs.get("minfos", []):
for info in minfo.get("infos", []):
uptime = int(float(info["time"]))
return uptime
except KeyError:
pass
return None
async def _get_wattage(self, web_devs: dict = None) -> Optional[int]:
if web_devs is None:
try:
web_devs = await self.web.devs()
except APIError:
pass
if web_devs is not None:
try:
for minfo in self.web_devs.get("minfos", []):
for info in minfo.get("infos", []):
wattage = int(float(info["power"]))
return wattage
except KeyError:
pass
return None

View File

@@ -1,5 +1,5 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# Copyright 2025 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
@@ -13,4 +13,4 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .Byte import GoldshellByte
from .byte import GoldshellByte

View File

@@ -13,7 +13,8 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .Byte import *
from .byte import *
from .mini_doge import *
from .X5 import *
from .XBox import *
from .XMax import *

View File

@@ -0,0 +1,16 @@
# ------------------------------------------------------------------------------
# Copyright 2025 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .mini_doge import GoldshellMiniDoge

View File

@@ -0,0 +1,179 @@
# ------------------------------------------------------------------------------
# Copyright 2025 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from typing import List, Optional
from pyasic.config import MinerConfig
from pyasic.data.boards import HashBoard
from pyasic.device.algorithm import AlgoHashRate
from pyasic.errors import APIError
from pyasic.logger import logger
from pyasic.miners.backends import GoldshellMiner
from pyasic.miners.data import (
DataFunction,
DataLocations,
DataOptions,
RPCAPICommand,
WebAPICommand,
)
from pyasic.miners.device.models import MiniDoge
GOLDSHELL_MINI_DOGE_DATA_LOC = DataLocations(
**{
str(DataOptions.MAC): DataFunction(
"_get_mac",
[WebAPICommand("web_setting", "setting")],
),
str(DataOptions.API_VERSION): DataFunction(
"_get_api_ver",
[RPCAPICommand("rpc_version", "version")],
),
str(DataOptions.FW_VERSION): DataFunction(
"_get_fw_ver",
[WebAPICommand("web_status", "status")],
),
str(DataOptions.HASHRATE): DataFunction(
"_get_hashrate",
[RPCAPICommand("rpc_summary", "summary")],
),
str(DataOptions.EXPECTED_HASHRATE): DataFunction(
"_get_expected_hashrate",
[RPCAPICommand("rpc_devs", "devs")],
),
str(DataOptions.HASHBOARDS): DataFunction(
"_get_hashboards",
[
RPCAPICommand("rpc_devs", "devs"),
RPCAPICommand("rpc_devdetails", "devdetails"),
],
),
str(DataOptions.FANS): DataFunction(
"_get_fans",
[RPCAPICommand("rpc_stats", "stats")],
),
str(DataOptions.POOLS): DataFunction(
"_get_pools",
[RPCAPICommand("rpc_pools", "pools")],
),
str(DataOptions.UPTIME): DataFunction(
"_get_uptime",
[WebAPICommand("web_devs", "devs")],
),
}
)
class GoldshellMiniDoge(GoldshellMiner, MiniDoge):
data_locations = GOLDSHELL_MINI_DOGE_DATA_LOC
supports_shutdown = False
supports_power_modes = False
async def get_config(self) -> MinerConfig:
try:
pools = await self.web.pools()
except APIError:
return self.config
self.config = MinerConfig.from_goldshell_list(pools)
return self.config
async def _get_expected_hashrate(
self, rpc_devs: dict = None
) -> Optional[AlgoHashRate]:
if rpc_devs is None:
try:
rpc_devs = await self.rpc.devs()
except APIError:
pass
if rpc_devs is not None:
try:
hash_rate = rpc_devs["DEVS"][0]["estimate_hash_rate"]
return self.algo.hashrate(
rate=float(hash_rate), unit=self.algo.unit.H
).into(self.algo.unit.default)
except KeyError:
pass
return None
async def _get_hashboards(
self, rpc_devs: dict = None, rpc_devdetails: dict = None
) -> List[HashBoard]:
if rpc_devs is None:
try:
rpc_devs = await self.rpc.devs()
except APIError:
pass
hashboards = [
HashBoard(slot=i, expected_chips=self.expected_chips)
for i in range(self.expected_hashboards)
]
if rpc_devs is not None:
if rpc_devs.get("DEVS"):
for board in rpc_devs["DEVS"]:
if board.get("ID") is not None:
try:
b_id = board["ID"]
hashboards[b_id].hashrate = self.algo.hashrate(
rate=float(board["MHS 20s"]), unit=self.algo.unit.MH
).into(self.algo.unit.default)
hashboards[b_id].chip_temp = board["tstemp-0"]
hashboards[b_id].temp = board["tstemp-1"]
hashboards[b_id].voltage = board["voltage"]
hashboards[b_id].active = board["Status"] == "Alive"
hashboards[b_id].missing = False
except KeyError:
pass
else:
logger.error(self, rpc_devs)
if rpc_devdetails is None:
try:
rpc_devdetails = await self.rpc.devdetails()
except APIError:
pass
if rpc_devdetails is not None:
if rpc_devdetails.get("DEVS"):
for board in rpc_devdetails["DEVS"]:
if board.get("ID") is not None:
try:
b_id = board["ID"]
hashboards[b_id].chips = board["chips-nr"]
except KeyError:
pass
else:
logger.error(self, rpc_devdetails)
return hashboards
async def _get_uptime(self, web_devs: dict = None) -> Optional[int]:
if web_devs is None:
try:
web_devs = await self.web.devs()
except APIError:
pass
if web_devs is not None:
try:
uptime = int(web_devs["data"][0]["time"])
return uptime
except KeyError:
pass
return None

View File

@@ -103,8 +103,12 @@ class GoldshellWebAPI(BaseWebAPI):
async with httpx.AsyncClient(transport=settings.transport()) as client:
for command in commands:
try:
uri_commnand = command
if command == "devs":
uri_commnand = "cgminer?cgminercmd=devs"
response = await client.get(
f"http://{self.ip}:{self.port}/mcb/{command}",
f"http://{self.ip}:{self.port}/mcb/{uri_commnand}",
headers={"Authorization": "Bearer " + self.token},
timeout=settings.get("api_function_timeout", 5),
)
@@ -143,3 +147,6 @@ class GoldshellWebAPI(BaseWebAPI):
async def status(self) -> dict:
return await self.send_command("status")
async def devs(self) -> dict:
return await self.send_command("cgminer?cgminercmd=devs")