from dataclasses import dataclass
import datetime
from enum import Enum, IntEnum, auto
from typing import Self
from .msglib import BasicMessage, EmptyMessage, StrMessage
[docs]class BatteryState(IntEnum):
#: Discharging
Ok = 0
#: Battery level is low, user should recharge
Low = 1
#: Battery is charging
Charging = 2
#: Battery is full and on craddle
Done = 3
#: Attempted to charge, but something went wrong (eg, coil voltage is wrong
#: from sitting crooked)
BadCharging = 4
#: WEIRDNESS (eg charging but no coil voltage)
Error = 5
[docs]class RollState_State(IntEnum):
"""
The current motion of the die.
"""
#:
Unknown = 0
#: The die is sitting flat and is not moving
OnFace = 1
#: The die is in hand (Note: I'm not sure how reliable the detection of
#: this state is.)
Handling = 2
#: The die is actively rolling
Rolling = 3
#: The die is still but not flat and level
Crooked = 4
[docs]class RequestMode(IntEnum):
"""
How much should a thing be reported.
(Called TelemetryRequestMode in other places.)
"""
#: Turn repeating off
Off = 0
#: Do a one-shot request
Once = 1
#: Repeatedly send the data until turned off.
Repeat = 2
[docs]@dataclass
class NoneMessage(EmptyMessage, id=0):
"""
Filler for message type 0.
"""
[docs]@dataclass
class WhoAreYou(EmptyMessage, id=1):
"""
Request some basic information.
Die replies with :class:`IAmADie`.
"""
[docs]class DieFlavor(Enum):
D4 = auto()
D6 = auto()
D6Pipped = auto()
D6Fudge = auto()
D8 = auto()
D10 = auto()
D12 = auto()
D20 = auto()
@staticmethod
def _from_led_count(leds: int) -> 'DieFlavor':
try:
return {
4: DieFlavor.D4,
6: DieFlavor.D6,
8: DieFlavor.D8,
10: DieFlavor.D10,
12: DieFlavor.D12,
20: DieFlavor.D20,
21: DieFlavor.D6Pipped,
# ???: DieFlavor.D6Fudge
}[leds]
except KeyError as exc:
raise ValueError("Unknown LED count: %i", leds) from exc
@property
def face_count(self) -> int:
"""
Return the number of faces this flavor has.
"""
return {
DieFlavor.D4: 4,
DieFlavor.D6: 6,
DieFlavor.D8: 8,
DieFlavor.D10: 10,
DieFlavor.D12: 12,
DieFlavor.D20: 20,
DieFlavor.D6Pipped: 6,
DieFlavor.D6Fudge: 6,
}[self]
[docs]class DesignAndColor(IntEnum):
Unknown = 0
Generic = 1
V3Orange = 2
V4BlackClear = 3
V4WhiteClear = 4
V5Grey = 5
V5White = 6
V5Black = 7
V5Gold = 8
OnyxBlack = 9
HematiteGrey = 10
MidnightGalaxy = 11
AuroraSky = 12
[docs]@dataclass
class IAmADie(BasicMessage, id=2, format="BB1xLLHL BB BB"):
"""
A bunch of general info.
Reply to :class:`WhoAreYou`
"""
#: Number of LEDs
led_count: int
#: The aesthetic design of the die
design_and_color: DesignAndColor
data_set_hash: int
#: The factory-assigned die ID
pixel_id: int
available_flash: int
#: Timestamp of when the firmware was built.
build_timestamp: datetime.datetime
#: Current roll state
roll_state: RollState_State
#: Current face that's up, starting at 0. Validity depends on :attr:`roll_state`.
roll_face: int
#: Current battery level as a percent
batt_level: int
#: Current battery percent
batt_state: BatteryState
@property
def flavor(self) -> DieFlavor:
"""
The kind of die this is, like D20 or Pipped D6
"""
return DieFlavor._from_led_count(self.led_count)
@property
def face_count(self) -> int:
"""
The total number of faces
"""
return self.flavor.face_count
@classmethod
def __struct_unpack__(cls, blob: bytes) -> Self:
self = super().__struct_unpack__(blob)
self.roll_state = RollState_State(self.roll_state)
self.batt_state = BatteryState(self.batt_state)
self.design_and_color = DesignAndColor(self.design_and_color)
self.build_timestamp = datetime.datetime.fromtimestamp(
self.build_timestamp, tz=datetime.timezone.utc)
return self
[docs] def to_rollstate(self) -> 'RollState':
"""
Repackages the rolling information.
"""
return RollState(
state=self.roll_state,
face=self.roll_face,
)
[docs] def to_batterylevel(self) -> 'BatteryLevel':
"""
Repackages the battery information.
"""
return BatteryLevel(
state=self.batt_state,
level=self.batt_level,
)
[docs]@dataclass
class RollState(BasicMessage, id=3, format="BB"):
"""
The current motion.
Broadcast on changes (see :meth:`.Pixel.handler()`) and is a reply to
:class:`RequestRollState`.
"""
#: The current motion
state: RollState_State
#: The upright face (starting at 0). Validity depends on :attr:`roll_state`.
face: int
@classmethod
def __struct_unpack__(cls, blob: bytes) -> Self:
self = super().__struct_unpack__(blob)
self.state = RollState_State(self.state)
return self
[docs]@dataclass
class Telemetry(BasicMessage, id=4, format="50x BBBB bB hh BB"):
# accelFrame: ... # TODO
battery_percent: int
battery_state: BatteryState
#: times 50
voltage: int
#: times 50
v_coil: int
rssi: int
#: 0-based
bt_channel: int
#: times 100
mcu_temp: int
#: times 100
battery_temp: int
internal_charge_state: int
force_disable_charging_state: int
[docs]@dataclass
class BulkSetup(BasicMessage, id=5, format=""):
...
[docs]@dataclass
class BulkSetupAck(BasicMessage, id=6, format=""):
...
[docs]@dataclass
class BulkData(BasicMessage, id=7, format=""):
...
[docs]@dataclass
class BulkDataAck(BasicMessage, id=8, format=""):
...
[docs]@dataclass
class TransferAnimationSet(BasicMessage, id=9, format=""):
...
[docs]@dataclass
class TransferAnimationSetAck(BasicMessage, id=10, format=""):
...
[docs]@dataclass
class TransferAnimationSetFinished(BasicMessage, id=11, format=""):
...
[docs]@dataclass
class TransferSettings(BasicMessage, id=12, format=""):
...
[docs]@dataclass
class TransferSettingsAck(BasicMessage, id=13, format=""):
...
[docs]@dataclass
class TransferSettingsFinished(BasicMessage, id=14, format=""):
...
[docs]@dataclass
class TransferTestAnimationSet(BasicMessage, id=15, format=""):
...
[docs]@dataclass
class TransferTestAnimationSetAck(BasicMessage, id=16, format=""):
...
[docs]@dataclass
class TransferTestAnimationSetFinished(BasicMessage, id=17, format=""):
...
[docs]@dataclass
class DebugLog(StrMessage, id=18):
text: str
@classmethod
def __struct_unpack__(cls, blob: bytes) -> Self:
return cls(
text=blob.decode('utf-8')
)
def __struct_pack__(self) -> bytes:
return self.text.encode('utf-8')
[docs]@dataclass
class PlayAnimation(BasicMessage, id=19, format="BBB"):
animation: int
remap_face: int
loop: int
[docs]@dataclass
class PlayAnimationEvent(BasicMessage, id=20, format="BBB"):
evt: int
remap_face: int
loop: int
[docs]@dataclass
class StopAnimation(BasicMessage, id=21, format="BB"):
animation: int
remap_face: int
[docs]@dataclass
class RemoteAction(BasicMessage, id=22, format="H"):
action_id: int
[docs]@dataclass
class RequestRollState(EmptyMessage, id=23):
"""
Request the current roll state.
Replied with :class:`RollState`.
"""
[docs]@dataclass
class RequestAnimationSet(BasicMessage, id=24, format=""):
...
[docs]@dataclass
class RequestSettings(BasicMessage, id=25, format=""):
...
[docs]@dataclass
class RequestTelemetry(BasicMessage, id=26, format=""):
...
[docs]@dataclass
class ProgramDefaultAnimationSet(BasicMessage, id=27, format=""):
...
[docs]@dataclass
class ProgramDefaultAnimationSetFinished(BasicMessage, id=28, format=""):
...
[docs]@dataclass
class Blink(BasicMessage, id=29, format="BHLLBB"):
"""
Do a custom blink.
Replied with :class:`BlinkAck`
"""
count: int
duration: int
color: int # TODO: RGB
face_mask: int # TODO: Enum?
fade: int
loop: int # TODO: bool/enum
[docs]@dataclass
class BlinkAck(EmptyMessage, id=30):
"""
Reply to :class:`Blink`.
"""
[docs]@dataclass
class RequestDefaultAnimationSetColor(BasicMessage, id=31, format=""):
...
[docs]@dataclass
class DefaultAnimationSetColor(BasicMessage, id=32, format=""):
...
[docs]@dataclass
class RequestBatteryLevel(EmptyMessage, id=33):
"""
Request the current battery.
Replies with :class:`BatteryLevel`.
"""
[docs]@dataclass
class BatteryLevel(BasicMessage, id=34, format="BB"):
"""
Current state of the battery.
Broadcast on changes (see :meth:`.Pixel.handler`) and reply to
:class:`RequestBatteryLevel`.
"""
#: The current level of the battery, as a percent
level: int
#: The current charge state
state: BatteryState
@classmethod
def __struct_unpack__(cls, blob: bytes) -> Self:
self = super().__struct_unpack__(blob)
self.state = BatteryState(self.state)
return self
[docs]@dataclass
class Calibrate(EmptyMessage, id=37):
"""
Start the calibration process.
"""
[docs]@dataclass
class CalibrateFace(BasicMessage, id=38, format="B"):
"""
Immediately calibrate the given face.
"""
face: int
[docs]@dataclass
class NotifyUser(BasicMessage, id=39, format="BBB"):
"""
Die asking the user a question
"""
#: How long the die will wait for a response, in seconds
timeout: int
#: Whether "ok" is an accepted answer
ok: bool
#: Whether "cancel" is an accepted answer
cancel: bool
#: Prompt to show the user
text: str
@classmethod
def __struct_unpack__(cls, blob: bytes) -> Self:
self = super().__struct_unpack__(blob)
self.ok = bool(self.ok)
self.cancel = bool(self.cancel)
return self
[docs]class OkCancel(IntEnum):
"""
Button enum for :class:`NotifyUserAck`
"""
Cancel = 0
Ok = 1
[docs]@dataclass
class NotifyUserAck(BasicMessage, id=40, format="B"):
"""
Response to :class:`NotifyUser`
"""
ok_cancel: OkCancel
@classmethod
def __struct_unpack__(cls, blob: bytes) -> Self:
self = super().__struct_unpack__(blob)
self.ok_cancel = OkCancel(self.ok_cancel)
return self
[docs]@dataclass
class TestHardware(BasicMessage, id=41, format=""):
...
[docs]@dataclass
class TestLEDLoopback(BasicMessage, id=42, format=""):
...
[docs]@dataclass
class LedLoopback(BasicMessage, id=43, format=""):
...
[docs]@dataclass
class SetTopLevelState(BasicMessage, id=44, format=""):
...
[docs]@dataclass
class ProgramDefaultParameters(BasicMessage, id=45, format=""):
...
[docs]@dataclass
class ProgramDefaultParametersFinished(BasicMessage, id=46, format=""):
...
[docs]@dataclass
class SetDesignAndColor(BasicMessage, id=47, format=""):
...
[docs]@dataclass
class SetDesignAndColorAck(BasicMessage, id=48, format=""):
...
[docs]@dataclass
class SetCurrentBehavior(BasicMessage, id=49, format=""):
...
[docs]@dataclass
class SetCurrentBehaviorAck(BasicMessage, id=50, format=""):
...
[docs]@dataclass
class SetName(StrMessage, id=51):
"""
Change the name of the die.
"""
name: str
[docs]@dataclass
class SetNameAck(EmptyMessage, id=52):
"""
Acknowledges :class:`SetName`.
"""
[docs]@dataclass
class Sleep(BasicMessage, id=53, format=""):
...
[docs]@dataclass
class ExitValidation(BasicMessage, id=54, format=""):
...
[docs]@dataclass
class TransferInstantAnimationSet(BasicMessage, id=55, format=""):
...
[docs]@dataclass
class TransferInstantAnimationSetAck(BasicMessage, id=56, format=""):
...
[docs]@dataclass
class TransferInstantAnimationSetFinished(BasicMessage, id=57, format=""):
...
[docs]@dataclass
class PlayInstantAnimation(BasicMessage, id=58, format=""):
...
[docs]@dataclass
class StopAllAnimations(EmptyMessage, id=59):
"""
Stop all animations.
"""
[docs]@dataclass
class RequestTemperature(EmptyMessage, id=60):
"""
Get the current temperature
Die replies with :class:`Temperature`.
"""
[docs]@dataclass
class Temperature(BasicMessage, id=61, format="hh"):
#: CPU temp in centidegrees Celsius
mcu_temp: int
#: Battery temp in centidgrees Celsius
batt_temp: int
[docs]@dataclass
class EnableCharging(BasicMessage, id=62, format=""):
...
[docs]@dataclass
class DisableCharging(BasicMessage, id=63, format=""):
...
[docs]@dataclass
class Discharge(BasicMessage, id=64, format=""):
...
[docs]@dataclass
class BlinkId(BasicMessage, id=65, format="BB"):
brightness: int
loop: int
[docs]@dataclass
class BlinkIdAck(EmptyMessage, id=66):
pass
# FIXME: Do these TransferTest* messages exist?
# FIXME: Are messages beyond this point numbered correctly?
[docs]@dataclass
class TransferTest(BasicMessage, id=67, format=""):
...
[docs]@dataclass
class TransferTestAck(BasicMessage, id=68, format=""):
...
[docs]@dataclass
class TransferTestFinished(BasicMessage, id=69, format=""):
...
[docs]@dataclass
class TestBulkSend(BasicMessage, id=70, format=""):
...
[docs]@dataclass
class TestBulkReceive(BasicMessage, id=71, format=""):
...
[docs]@dataclass
class SetAllLEDsToColor(BasicMessage, id=72, format=""):
...
[docs]@dataclass
class AttractMode(BasicMessage, id=73, format=""):
...
[docs]@dataclass
class PrintNormals(BasicMessage, id=74, format=""):
...
[docs]@dataclass
class PrintA2DReadings(BasicMessage, id=75, format=""):
...
[docs]@dataclass
class LightUpFace(BasicMessage, id=76, format=""):
...
[docs]@dataclass
class SetLEDToColor(BasicMessage, id=77, format=""):
...
[docs]@dataclass
class DebugAnimationController(BasicMessage, id=78, format=""):
...