Skip to content

Commit c0e2281

Browse files
authored
Merge pull request #14 from Windham-High-School/10-time-tools
10 time tools
2 parents a1d55bb + f9cb7bc commit c0e2281

File tree

5 files changed

+249
-12
lines changed

5 files changed

+249
-12
lines changed

servercom/_utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""Some utilities that help out behind-the-scenes."""
2+
3+
class Immutable:
4+
"""Superclass for immutable objects
5+
"""
6+
7+
def __init__(self):
8+
"""As soon as this constructor is called, the object becomes immutable
9+
"""
10+
self._frozen = True
11+
12+
def __delattr__(self, *args, **kwargs):
13+
if hasattr(self, '_frozen'):
14+
raise AttributeError("This object is immutable. You cannot delete instance variables from it.")
15+
object.__delattr__(self, *args, **kwargs)
16+
17+
def __setattr__(self, *args, **kwargs):
18+
if hasattr(self, '_frozen'):
19+
raise AttributeError("This object is immutable. You cannot set any instance variables.")
20+
object.__setattr__(self, *args, **kwargs)
21+
22+
def enum(**enums):
23+
"""Fake enum-maker"""
24+
return type('Enum', (), enums)

servercom/implementations/circuitpy.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""CircuitPython implementation of the CubeServer API Wrapper Library"""
22

33
from .common import *
4+
from ..timetools import Time
5+
from .._utils import enum
46

57
import ssl
68
import wifi
@@ -21,10 +23,6 @@ def _replace_code(new_code: bytes, do_reset=True):
2123
if do_reset:
2224
reset()
2325

24-
def enum(**enums):
25-
"""Fake enum-maker"""
26-
return type('Enum', (), enums)
27-
2826
def basic_auth_str(user: str, pwd: str) -> str:
2927
"""Encodes the username and password as per RFC7617 on Basic Auth"""
3028
return b2a_base64(f"{user}:{pwd}".encode()).strip().decode("utf-8")
@@ -379,6 +377,9 @@ def request(
379377
raise last_error
380378

381379
def get_status(self) -> GameStatus:
380+
"""Gives the team access to their score and the current unix time.
381+
Most teams probably won't need this.
382+
"""
382383
if self.v:
383384
print("Getting status...")
384385
resp = self.request('GET', '/status',
@@ -387,9 +388,18 @@ def get_status(self) -> GameStatus:
387388
resp_json = loads(resp[1])
388389
if self.v:
389390
print(f"It is {resp_json['unix_time']} seconds since the epoch.")
390-
return GameStatus(resp_json['unix_time'], resp_json['status']['score'], resp_json['status']['strikes'])
391+
return GameStatus(Time(resp_json['unix_time']), resp_json['status']['score'])
392+
393+
def sync_time(self) -> bool:
394+
"""Syncs the current clock against the server"""
395+
status = self.get_status()
396+
if status:
397+
Time.set_time(status.time)
398+
return True
399+
return False
391400

392401
def post(self, point: DataPoint) -> bool:
402+
"""Posts a DataPoint object to the server"""
393403
if self.v:
394404
print("Posting datapoint!")
395405
return self.request(
@@ -401,6 +411,7 @@ def post(self, point: DataPoint) -> bool:
401411
).code == 201
402412

403413
def email(self, msg: Email) -> bool:
414+
"""Sends an email to the team"""
404415
if self.v:
405416
print(f"Sending email {msg.subject}...")
406417
return self.request(

servercom/implementations/common.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@
1414

1515

1616
GameStatus = namedtuple("GameStatus",
17-
['unix_time',
18-
'score',
19-
'strikes']
17+
['time',
18+
'score']
2019
)
2120

2221
HTTPResponse = namedtuple("HTTPResponse",
@@ -31,7 +30,7 @@ def conf_if_exists(key: str):
3130

3231
class ConnectionConfig:
3332
"""The configuration of the connection to the server"""
34-
TIMEOUT: int = 60
33+
TIMEOUT: int = 10
3534
if 'client_config' in globals():
3635
AP_SSID: str = client_config.CONF_AP_SSID
3736
API_CN: str = client_config.CONF_API_CN

servercom/implementations/cpy.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""CPython implementation of the CubeServer API Wrapper Library"""
22

33
from .common import *
4+
from ..timetools import Time
45

56
import ssl
67
import socket
@@ -338,27 +339,44 @@ def request(
338339
raise last_error
339340

340341
def get_status(self) -> GameStatus:
342+
"""Gives the team access to their score and the current unix time.
343+
Most teams probably won't need this.
344+
"""
341345
resp = self.request('GET', '/status')
342346
resp_json = loads(resp[1])
343-
return GameStatus(resp_json['unix_time'], resp_json['status']['score'], resp_json['status']['strikes'])
347+
return GameStatus(Time(resp_json['unix_time']), resp_json['status']['score'])
348+
349+
def sync_time(self) -> bool:
350+
"""Syncs the current clock against the server"""
351+
status = self.get_status()
352+
if status:
353+
Time.set_time(status.time)
354+
return True
355+
return False
344356

345357
def post(self, point: DataPoint) -> bool:
358+
"""Posts a DataPoint object to the server"""
346359
return self.request(
347360
'POST',
348361
'/data',
349362
point.dumps(),
350363
content_type = 'application/json',
351-
headers=['User-Agent: CPython, dude!']
364+
headers=['User-Agent: CPythonDude']
352365
).code == 201
353366
def email(self, msg: Email) -> bool:
367+
"""Sends an email to the team"""
354368
return self.request(
355369
'POST',
356370
'/email',
357371
msg.dumps(),
358372
content_type = 'application/json',
359-
headers=['User-Agent: CPython, dude!']
373+
headers=['User-Agent: CPythonDude']
360374
).code == 201
361375
def code_update(reset=True):
376+
"""THIS FEATURE IS NOT IMPLEMENTED FOR CPython.
377+
Checks for a code update from the server.
378+
Restarts the microcontroller and run the new code if it is available
379+
"""
362380
pass
363381
def __exit__(self):
364382
if self.v:

servercom/timetools.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
"""Time Tools
2+
Some tools for dealing with times on microcontrollers
3+
(to a precision of 1 second)
4+
"""
5+
6+
from time import monotonic, struct_time
7+
from ._utils import Immutable, enum
8+
9+
# Time unit declarations in seconds:
10+
TimeUnit = enum(
11+
SECOND = 1,
12+
MINUTE = 60,
13+
HOUR = 3600,
14+
DAY = 3600 * 24,
15+
WEEK = 3600 * 24 * 7,
16+
YEAR = 3600 * 24 * 365
17+
)
18+
19+
# Time zone UTC offsets:
20+
TimeZone = enum(
21+
UTC = 0,
22+
GMT = 0,
23+
EST = -5 * TimeUnit.HOUR,
24+
EDT = -4 * TimeUnit.HOUR
25+
)
26+
27+
class Time(Immutable):
28+
"""Represents an instant in time based around a UNIX timestamp"""
29+
30+
# (monotonic time, epoch time) where both represent the same physical time
31+
_timeset = (None, None)
32+
_time_offset = None
33+
34+
# Class Methods:
35+
@classmethod
36+
def set_unix_time(cls, unix_timestamp: int) -> None:
37+
cls._timeset = (int(monotonic()), int(unix_timestamp))
38+
cls._time_offset = int(unix_timestamp - monotonic())
39+
40+
@classmethod
41+
def set_time(cls, current_time: 'Time') -> None:
42+
cls.set_unix_time(current_time.seconds)
43+
44+
@classmethod
45+
def from_unix_time(cls, unix_time: int) -> int:
46+
"""Returns a monotonic timestamp from a unix timestamp"""
47+
return int(unix_time - cls._time_offset)
48+
49+
@classmethod
50+
def from_monotonic_time(cls, monotonic_time: int) -> int:
51+
"""Returns a UNIX timestamp for a given monotonic time"""
52+
return int(monotonic_time + cls._time_offset)
53+
54+
@classmethod
55+
def get_unix_time(cls) -> int:
56+
return cls.from_monotonic_time(monotonic())
57+
58+
@classmethod
59+
def now(cls) -> 'Time':
60+
return cls(cls.get_unix_time())
61+
62+
# Constructor and Instance Methods:
63+
def __init__(
64+
self,
65+
value: int,
66+
unit: TimeUnit = TimeUnit.SECOND,
67+
absolute: bool = True
68+
):
69+
self.seconds = value * unit
70+
self.absolute = absolute
71+
super().__init__()
72+
73+
def offset(self, seconds:int=0, minutes:int=0, hours:int=0, days:int=0, weeks:int=0, years:int=0) -> 'Time':
74+
"""Offsets this time by a given amount
75+
76+
Args:
77+
seconds (int, optional): seconds to offset by. Defaults to 0.
78+
minutes (int, optional): minutes to offset by. Defaults to 0.
79+
hours (int, optional): hours to offset by. Defaults to 0.
80+
days (int, optional): days to offset by. Defaults to 0.
81+
weeks (int, optional): weeks to offset by. Defaults to 0.
82+
years (int, optional): years to offset by. Defaults to 0.
83+
84+
Returns:
85+
Time: Returns self to allow daisy-chain operations
86+
"""
87+
return Time(self.seconds
88+
+ seconds
89+
+ minutes * TimeUnit.MINUTE
90+
+ hours * TimeUnit.HOUR
91+
+ days * TimeUnit.DAY
92+
+ weeks * TimeUnit.WEEK
93+
+ years * TimeUnit.YEAR
94+
)
95+
96+
def since_last(self, timeunit: int) -> 'Time':
97+
"""Returns a Time object representing time since the beginning of the
98+
most recent <MINUTE|HOUR|DAY|WEEK|YEAR>
99+
100+
Args:
101+
timeunit (int): the length, in seconds, of the specified time unit
102+
103+
Returns:
104+
Time: the new representative Time object
105+
"""
106+
return Time(
107+
self.seconds % timeunit,
108+
absolute=False
109+
)
110+
111+
def __add__(self, other: 'Time') -> 'Time':
112+
if not isinstance(other, Time):
113+
raise TypeError(f"Cannot add {type(other)} to Time")
114+
return Time(
115+
self.seconds + other.seconds,
116+
absolute=False
117+
)
118+
119+
def __sub__(self, other: 'Time') -> 'Time':
120+
if not isinstance(other, Time):
121+
raise TypeError(f"Cannot subtract {type(other)} from Time")
122+
return Time(
123+
self.seconds - other.seconds,
124+
absolute=False
125+
)
126+
127+
def elapsed_since(self, other: 'Time') -> 'Time':
128+
""" Returns time elapsed since a given time
129+
"""
130+
return self - other
131+
132+
def elapsed_until(self, other: 'Time') -> 'Time':
133+
""" Returns time elapsed between this and a future time
134+
"""
135+
return other - self
136+
137+
def __mul__(self, other) -> 'Time':
138+
if not (isinstance(other, int) or isinstance(other, float)):
139+
raise TypeError(f"You can only multiply Time objects by ints and floats")
140+
return Time(
141+
self.seconds * other,
142+
absolute=False
143+
)
144+
145+
def __div__(self, other) -> 'Time':
146+
if not (isinstance(other, int) or isinstance(other, float)):
147+
raise TypeError(f"You can only divide Time objects by ints and floats")
148+
return Time(
149+
self.seconds // other,
150+
absolute=False
151+
)
152+
153+
def __floordiv__(self, other: int) -> int:
154+
if not isinstance(other, int) or self.absolute:
155+
raise TypeError("Floor division is only to be used as Time // TimeUnit (int) to find the number of a unit that fits in a given relative time/time delta")
156+
return self.seconds // other
157+
158+
def __mod__(self, other: 'Time') -> 'Time':
159+
if not (isinstance(other, Time) or isinstance(other, int)):
160+
raise TypeError(f"Cannot modulo {type(other)} with Time")
161+
return self.since_last(int(other))
162+
163+
def __str__(self) -> str:
164+
if self.absolute:
165+
return f"{self.seconds} seconds since 1 January 1970 00:00:00 UTC"
166+
return f"{self.seconds} seconds"
167+
168+
def __repr__(self) -> str:
169+
return self.__str__()
170+
171+
def __abs__(self) -> 'Time':
172+
return Time(
173+
abs(self.seconds),
174+
self.absolute
175+
)
176+
177+
def __int__(self) -> int:
178+
return self.seconds
179+
180+
@property
181+
def monotonic(self) -> int:
182+
"""Returns the equivalent monotonic time"""
183+
if self.absolute:
184+
return self.from_unix_time(self.seconds)
185+
return monotonic() + self.seconds

0 commit comments

Comments
 (0)