nightsky.gno
7.92 Kb · 264 lines
1// Package nightsky holds the shared types and single-telescope logic for the
2// GnoNightSky telescope network.
3//
4// Note on panics: although this lives under p/, it is application-specific
5// logic for the NightSky realms rather than a general-purpose reusable
6// library. It deliberately panics on invalid input / failed preconditions
7// (the assert-like style described in effective-gno) so the wrapping realms
8// don't have to re-check every error. Callers that want softer handling
9// should validate before calling.
10package nightsky
11
12import (
13 "time"
14
15 "gno.land/p/nt/avl/v0"
16)
17
18// TelescopeConfig contains the settings and configuration for a telescope
19type TelescopeConfig struct {
20 Name string // Name of the telescope
21 Model string // Telescope model (e.g., "Seestar S50")
22 Latitude float64 // Geographic location
23 Longitude float64 // Geographic location
24 Owner string // Owner's gno address
25 RealmPath string // Path to the telescope's realm (e.g., "/r/username/telescope")
26 Status string // Current status: "online", "offline", "busy", "error"
27 LastUpdate time.Time // Last status update
28}
29
30// AccessRule defines who can access a telescope
31type AccessRule struct {
32 AllowedAddress string // Gno address allowed to access
33 GrantedBy string // Who granted access
34 GrantedAt time.Time // When access was granted
35 ExpiresAt time.Time // When access expires (zero time = never)
36}
37
38// TelescopeCommand represents a command sent to a telescope
39type TelescopeCommand struct {
40 CommandType string // "capture" or "stop"
41 TargetRA float64 // Right Ascension (for pointing/tracking)
42 TargetDec float64 // Declination (for pointing/tracking)
43 Exposure int // Exposure time in seconds (for capture)
44 RequestedBy string // Who requested the command
45 RequestedAt time.Time // When the command was requested
46}
47
48// CaptureResult represents the result of an image capture
49type CaptureResult struct {
50 ImageURL string // URL to the captured image (e.g., Imgur)
51 TargetRA float64 // Right Ascension of target
52 TargetDec float64 // Declination of target
53 Exposure int // Exposure time in seconds
54 CapturedBy string // Who captured the image
55 CapturedAt time.Time // When the image was captured
56 TelescopeID string // Which telescope captured it
57}
58
59// NewTelescopeConfig creates a new telescope configuration
60func NewTelescopeConfig(name, model string, lat, lon float64, owner, realmPath string) TelescopeConfig {
61 return TelescopeConfig{
62 Name: name,
63 Model: model,
64 Latitude: lat,
65 Longitude: lon,
66 Owner: owner,
67 RealmPath: realmPath,
68 Status: "offline",
69 LastUpdate: time.Now(),
70 }
71}
72
73// UpdateStatus updates the telescope status
74func (t *TelescopeConfig) UpdateStatus(status string) {
75 t.Status = status
76 t.LastUpdate = time.Now()
77}
78
79// IsAccessAllowed checks if an address has a current (non-expired) access rule.
80// rules is an avl.Tree keyed by allowed address holding AccessRule values.
81func IsAccessAllowed(address string, rules *avl.Tree) bool {
82 value, ok := rules.Get(address)
83 if !ok {
84 return false
85 }
86 rule := value.(AccessRule)
87 return rule.ExpiresAt.IsZero() || time.Now().Before(rule.ExpiresAt)
88}
89
90// ValidateCommand validates a telescope command
91func ValidateCommand(cmd TelescopeCommand, config TelescopeConfig) {
92 switch cmd.CommandType {
93 case "capture", "stop":
94 default:
95 panic("invalid command type: must be capture or stop")
96 }
97
98 if cmd.CommandType == "capture" {
99 if cmd.TargetRA < 0 || cmd.TargetRA >= 24 {
100 panic("invalid RA: must be between 0 and 24 hours")
101 }
102 if cmd.TargetDec < -90 || cmd.TargetDec > 90 {
103 panic("invalid Dec: must be between -90 and 90 degrees")
104 }
105 if cmd.Exposure < 1 || cmd.Exposure > 300 {
106 panic("invalid exposure time: must be between 1 and 300 seconds")
107 }
108 }
109}
110
111// maxCaptureHistory is the number of captures a single telescope keeps locally.
112const maxCaptureHistory = 100
113
114// TelescopeRealm manages a single telescope's state and operations
115type TelescopeRealm struct {
116 Config *TelescopeConfig
117 AccessRules *avl.Tree // key: allowed address -> AccessRule
118 CommandQueue []TelescopeCommand
119 Captures *CaptureFeed
120 OwnerAddress string
121}
122
123// NewTelescopeRealm creates a new telescope realm instance
124func NewTelescopeRealm(config *TelescopeConfig) *TelescopeRealm {
125 return &TelescopeRealm{
126 Config: config,
127 AccessRules: avl.NewTree(),
128 CommandQueue: []TelescopeCommand{},
129 Captures: NewCaptureFeed(maxCaptureHistory),
130 OwnerAddress: config.Owner,
131 }
132}
133
134// GrantAccess allows the owner to grant access to another user. Re-granting to
135// an existing address overwrites the previous rule.
136func (r *TelescopeRealm) GrantAccess(caller, address string, durationDays int) {
137 r.checkOwner(caller)
138
139 if address == "" {
140 panic("address cannot be empty")
141 }
142
143 expiresAt := time.Time{} // Never expires by default
144 if durationDays > 0 {
145 expiresAt = time.Now().Add(time.Duration(durationDays) * 24 * time.Hour)
146 }
147
148 rule := AccessRule{
149 AllowedAddress: address,
150 GrantedBy: r.OwnerAddress,
151 GrantedAt: time.Now(),
152 ExpiresAt: expiresAt,
153 }
154
155 r.AccessRules.Set(address, rule)
156}
157
158// RevokeAccess removes access for a specific address
159func (r *TelescopeRealm) RevokeAccess(caller, address string) {
160 r.checkOwner(caller)
161 r.AccessRules.Remove(address)
162}
163
164// SubmitCommand allows authorized users to submit telescope commands
165func (r *TelescopeRealm) SubmitCommand(caller, commandType string, targetRA, targetDec float64, exposure int) {
166 // Check if caller has access (owner always has access)
167 if caller != r.OwnerAddress {
168 if !IsAccessAllowed(caller, r.AccessRules) {
169 panic("access denied: you don't have permission to use this telescope")
170 }
171 }
172
173 // Create command
174 cmd := TelescopeCommand{
175 CommandType: commandType,
176 TargetRA: targetRA,
177 TargetDec: targetDec,
178 Exposure: exposure,
179 RequestedBy: caller,
180 RequestedAt: time.Now(),
181 }
182
183 // Validate command
184 ValidateCommand(cmd, *r.Config)
185
186 // Add to queue
187 r.CommandQueue = append(r.CommandQueue, cmd)
188
189 // Update telescope status to busy
190 r.Config.UpdateStatus("busy")
191}
192
193// GetNextCommand returns the next command in the queue (for telescope controller)
194func (r *TelescopeRealm) GetNextCommand(caller string) TelescopeCommand {
195 r.checkOwner(caller)
196
197 if len(r.CommandQueue) == 0 {
198 panic("no commands in queue")
199 }
200
201 cmd := r.CommandQueue[0]
202 r.CommandQueue = r.CommandQueue[1:]
203
204 return cmd
205}
206
207// UpdateStatus allows the owner to update telescope status
208func (r *TelescopeRealm) UpdateStatus(caller string, status string) {
209 r.checkOwner(caller)
210 r.Config.UpdateStatus(status)
211}
212
213// RecordCapture allows the owner to record a capture result
214func (r *TelescopeRealm) RecordCapture(caller, imageURL string, targetRA, targetDec float64, exposure int, capturedBy string) {
215 r.checkOwner(caller)
216
217 if imageURL == "" {
218 panic("image URL cannot be empty")
219 }
220
221 capture := CaptureResult{
222 ImageURL: imageURL,
223 TargetRA: targetRA,
224 TargetDec: targetDec,
225 Exposure: exposure,
226 CapturedBy: capturedBy,
227 CapturedAt: time.Now(),
228 TelescopeID: r.Config.Name,
229 }
230
231 r.Captures.Add(capture)
232
233 // Update status back to online
234 r.Config.UpdateStatus("online")
235}
236
237// ClearCommandQueue clears all pending commands (emergency stop)
238func (r *TelescopeRealm) ClearCommandQueue(caller string) {
239 r.checkOwner(caller)
240 r.CommandQueue = []TelescopeCommand{}
241 r.Config.UpdateStatus("online")
242}
243
244// GetCommandCount returns the number of pending commands
245func (r *TelescopeRealm) GetCommandCount() int {
246 return len(r.CommandQueue)
247}
248
249// GetCaptureCount returns the number of stored captures
250func (r *TelescopeRealm) GetCaptureCount() int {
251 return r.Captures.Len()
252}
253
254// GetAccessRuleCount returns the number of access rules
255func (r *TelescopeRealm) GetAccessRuleCount() int {
256 return r.AccessRules.Size()
257}
258
259// checkOwner verifies the caller is the owner
260func (r *TelescopeRealm) checkOwner(caller string) {
261 if caller != r.OwnerAddress {
262 panic("access restricted: owner only")
263 }
264}