· 4 years ago · Feb 06, 2021, 03:04 PM
1--## MADE USE ONE CHANNEL AS PROFF OF CONCEPT. YOU CAN STOP YAMMERING ABOUT IT SKY. I CAN'T TAKE IT ANYMORE AHHHHHHHHHHHHHHH.
2--## Documentation not modified. F you whoever decides to use this. Docs are wrong now.
3
4--- The Rednet API allows systems to communicate between each other without
5-- using redstone. It serves as a wrapper for the modem API, offering ease of
6-- functionality (particularly in regards to repeating signals) with some
7-- expense of fine control.
8--
9-- In order to send and receive data, a modem (either wired, wireless, or ender)
10-- is required. The data reaches any possible destinations immediately after
11-- sending it, but is range limited.
12--
13-- Rednet also allows you to use a "protocol" - simple string names indicating
14-- what messages are about. Receiving systems may filter messages according to
15-- their protocols, thereby automatically ignoring incoming messages which don't
16-- specify an identical string. It's also possible to @{rednet.lookup|lookup}
17-- which systems in the area use certain protocols, hence making it easier to
18-- determine where given messages should be sent in the first place.
19--
20-- @module rednet
21
22local expect = dofile("rom/modules/main/cc/expect.lua").expect
23
24--- The channel used by the Rednet API to @{broadcast} messages.
25CHANNEL_BROADCAST = 65535
26
27--- The channel used by the Rednet API to repeat messages.
28CHANNEL_REPEAT = 65533
29
30local tReceivedMessages = {}
31local tReceivedMessageTimeouts = {}
32local tHostnames = {}
33
34--- Opens a modem with the given @{peripheral} name, allowing it to send and
35-- receive messages over rednet.
36--
37-- This will open the modem on two channels: one which has the same
38-- @{os.getComputerID|ID} as the computer, and another on
39-- @{CHANNEL_BROADCAST|the broadcast channel}.
40--
41-- @tparam string modem The name of the modem to open.
42-- @throws If there is no such modem with the given name
43function open(modem)
44 expect(1, modem, "string")
45 if peripheral.getType(modem) ~= "modem" then
46 error("No such modem: " .. modem, 2)
47 end
48 peripheral.call(modem, "open", CHANNEL_REPEAT)
49 peripheral.call(modem, "open", CHANNEL_BROADCAST)
50end
51
52--- Close a modem with the given @{peripheral} name, meaning it can no longer
53-- send and receive rednet messages.
54--
55-- @tparam[opt] string modem The side the modem exists on. If not given, all
56-- open modems will be closed.
57-- @throws If there is no such modem with the given name
58function close(modem)
59 expect(1, modem, "string", "nil")
60 if modem then
61 -- Close a specific modem
62 if peripheral.getType(modem) ~= "modem" then
63 error("No such modem: " .. modem, 2)
64 end
65 peripheral.call(modem, "close", CHANNEL_REPEAT)
66 peripheral.call(modem, "close", CHANNEL_BROADCAST)
67 else
68 -- Close all modems
69 for _, modem in ipairs(peripheral.getNames()) do
70 if isOpen(modem) then
71 close(modem)
72 end
73 end
74 end
75end
76
77--- Determine if rednet is currently open.
78--
79-- @tparam[opt] string modem Which modem to check. If not given, all connected
80-- modems will be checked.
81-- @treturn boolean If the given modem is open.
82function isOpen(modem)
83 expect(1, modem, "string", "nil")
84 if modem then
85 -- Check if a specific modem is open
86 if peripheral.getType(modem) == "modem" then
87 return peripheral.call(modem, "isOpen", CHANNEL_REPEAT) and peripheral.call(modem, "isOpen", CHANNEL_BROADCAST)
88 end
89 else
90 -- Check if any modem is open
91 for _, modem in ipairs(peripheral.getNames()) do
92 if isOpen(modem) then
93 return true
94 end
95 end
96 end
97 return false
98end
99
100--- Allows a computer or turtle with an attached modem to send a message
101-- intended for a system with a specific ID. At least one such modem must first
102-- be @{rednet.open|opened} before sending is possible.
103--
104-- Assuming the target was in range and also had a correctly opened modem, it
105-- may then use @{rednet.receive} to collect the message.
106--
107-- @tparam number nRecipient The ID of the receiving computer.
108-- @param message The message to send. This should not contain coroutines or
109-- functions, as they will be converted to @{nil}.
110-- @tparam[opt] string sProtocol The "protocol" to send this message under. When
111-- using @{rednet.receive} one can filter to only receive messages sent under a
112-- particular protocol.
113-- @treturn boolean If this message was successfully sent (i.e. if rednet is
114-- currently @{rednet.open|open}). Note, this does not guarantee the message was
115-- actually _received_.
116-- @see rednet.receive
117function send(nRecipient, message, sProtocol)
118 expect(1, nRecipient, "number")
119 expect(3, sProtocol, "string", "nil")
120 -- Generate a (probably) unique message ID
121 -- We could do other things to guarantee uniqueness, but we really don't need to
122 -- Store it to ensure we don't get our own messages back
123 local nMessageID = math.random(1, 2147483647)
124 tReceivedMessages[nMessageID] = true
125 tReceivedMessageTimeouts[os.startTimer(30)] = nMessageID
126
127 -- Create the message
128 local nReplyChannel = os.getComputerID()
129 local tMessage = {
130 nMessageID = nMessageID,
131 nRecipient = nRecipient,
132 nReplyChannel = nReplyChannel,
133 message = message,
134 sProtocol = sProtocol,
135 }
136
137 local sent = false
138 if nRecipient == os.getComputerID() then
139 -- Loopback to ourselves
140 os.queueEvent("rednet_message", nReplyChannel, message, sProtocol)
141 sent = true
142 else
143 -- Send on all open modems, to the target and to repeaters
144 for _, sModem in ipairs(peripheral.getNames()) do
145 if isOpen(sModem) then
146 --peripheral.call(sModem, "transmit", nRecipient, nReplyChannel, tMessage)
147 peripheral.call(sModem, "transmit", CHANNEL_REPEAT, CHANNEL_REPEAT, tMessage)
148 sent = true
149 end
150 end
151 end
152
153 return sent
154end
155
156--- Broadcasts a string message over the predefined @{CHANNEL_BROADCAST}
157-- channel. The message will be received by every device listening to rednet.
158--
159-- @param message The message to send. This should not contain coroutines or
160-- functions, as they will be converted to @{nil}.
161-- @tparam[opt] string sProtocol The "protocol" to send this message under. When
162-- using @{rednet.receive} one can filter to only receive messages sent under a
163-- particular protocol.
164-- @see rednet.receive
165function broadcast(message, sProtocol)
166 expect(2, sProtocol, "string", "nil")
167 send(CHANNEL_BROADCAST, message, sProtocol)
168end
169
170--- Wait for a rednet message to be received, or until `nTimeout` seconds have
171-- elapsed.
172--
173-- @tparam[opt] string sProtocolFilter The protocol the received message must be
174-- sent with. If specified, any messages not sent under this protocol will be
175-- discarded.
176-- @tparam[opt] number nTimeout The number of seconds to wait if no message is
177-- received.
178-- @treturn[1] number The computer which sent this message
179-- @return[1] The received message
180-- @treturn[1] string|nil The protocol this message was sent under.
181-- @treturn[2] nil If the timeout elapsed and no message was received.
182-- @see rednet.broadcast
183-- @see rednet.send
184function receive(sProtocolFilter, nTimeout)
185 -- The parameters used to be ( nTimeout ), detect this case for backwards compatibility
186 if type(sProtocolFilter) == "number" and nTimeout == nil then
187 sProtocolFilter, nTimeout = nil, sProtocolFilter
188 end
189 expect(1, sProtocolFilter, "string", "nil")
190 expect(2, nTimeout, "number", "nil")
191
192 -- Start the timer
193 local timer = nil
194 local sFilter = nil
195 if nTimeout then
196 timer = os.startTimer(nTimeout)
197 sFilter = nil
198 else
199 sFilter = "rednet_message"
200 end
201
202 -- Wait for events
203 while true do
204 local sEvent, p1, p2, p3 = os.pullEvent(sFilter)
205 if sEvent == "rednet_message" then
206 -- Return the first matching rednet_message
207 local nSenderID, message, sProtocol = p1, p2, p3
208 if sProtocolFilter == nil or sProtocol == sProtocolFilter then
209 return nSenderID, message, sProtocol
210 end
211 elseif sEvent == "timer" then
212 -- Return nil if we timeout
213 if p1 == timer then
214 return nil
215 end
216 end
217 end
218end
219
220--- Register the system as "hosting" the desired protocol under the specified
221-- name. If a rednet @{rednet.lookup|lookup} is performed for that protocol (and
222-- maybe name) on the same network, the registered system will automatically
223-- respond via a background process, hence providing the system performing the
224-- lookup with its ID number.
225--
226-- Multiple computers may not register themselves on the same network as having
227-- the same names against the same protocols, and the title `localhost` is
228-- specifically reserved. They may, however, share names as long as their hosted
229-- protocols are different, or if they only join a given network after
230-- "registering" themselves before doing so (eg while offline or part of a
231-- different network).
232--
233-- @tparam string sProtocol The protocol this computer provides.
234-- @tparam string sHostname The name this protocol exposes for the given protocol.
235-- @throws If trying to register a hostname which is reserved, or currently in use.
236-- @see rednet.unhost
237-- @see rednet.lookup
238function host(sProtocol, sHostname)
239 expect(1, sProtocol, "string")
240 expect(2, sHostname, "string")
241 if sHostname == "localhost" then
242 error("Reserved hostname", 2)
243 end
244 if tHostnames[sProtocol] ~= sHostname then
245 if lookup(sProtocol, sHostname) ~= nil then
246 error("Hostname in use", 2)
247 end
248 tHostnames[sProtocol] = sHostname
249 end
250end
251
252--- Stop @{rednet.host|hosting} a specific protocol, meaning it will no longer
253-- respond to @{rednet.lookup} requests.
254--
255-- @tparam string sProtocol The protocol to unregister your self from.
256function unhost(sProtocol)
257 expect(1, sProtocol, "string")
258 tHostnames[sProtocol] = nil
259end
260
261--- Search the local rednet network for systems @{rednet.host|hosting} the
262-- desired protocol and returns any computer IDs that respond as "registered"
263-- against it.
264--
265-- If a hostname is specified, only one ID will be returned (assuming an exact
266-- match is found).
267--
268-- @tparam string sProtocol The protocol to search for.
269-- @tparam[opt] string sHostname The hostname to search for.
270--
271-- @treturn[1] { number }|nil A list of computer IDs hosting the given
272-- protocol, or @{nil} if none exist.
273-- @treturn[2] number|nil The computer ID with the provided hostname and protocol,
274-- or @{nil} if none exists.
275function lookup(sProtocol, sHostname)
276 expect(1, sProtocol, "string")
277 expect(2, sHostname, "string", "nil")
278
279 -- Build list of host IDs
280 local tResults = nil
281 if sHostname == nil then
282 tResults = {}
283 end
284
285 -- Check localhost first
286 if tHostnames[sProtocol] then
287 if sHostname == nil then
288 table.insert(tResults, os.getComputerID())
289 elseif sHostname == "localhost" or sHostname == tHostnames[sProtocol] then
290 return os.getComputerID()
291 end
292 end
293
294 if not isOpen() then
295 if tResults then
296 return table.unpack(tResults)
297 end
298 return nil
299 end
300
301 -- Broadcast a lookup packet
302 broadcast({
303 sType = "lookup",
304 sProtocol = sProtocol,
305 sHostname = sHostname,
306 }, "dns")
307
308 -- Start a timer
309 local timer = os.startTimer(2)
310
311 -- Wait for events
312 while true do
313 local event, p1, p2, p3 = os.pullEvent()
314 if event == "rednet_message" then
315 -- Got a rednet message, check if it's the response to our request
316 local nSenderID, tMessage, sMessageProtocol = p1, p2, p3
317 if sMessageProtocol == "dns" and type(tMessage) == "table" and tMessage.sType == "lookup response" then
318 if tMessage.sProtocol == sProtocol then
319 if sHostname == nil then
320 table.insert(tResults, nSenderID)
321 elseif tMessage.sHostname == sHostname then
322 return nSenderID
323 end
324 end
325 end
326 else
327 -- Got a timer event, check it's the end of our timeout
328 if p1 == timer then
329 break
330 end
331 end
332 end
333 if tResults then
334 return table.unpack(tResults)
335 end
336 return nil
337end
338
339local bRunning = false
340
341--- Listen for modem messages and converts them into rednet messages, which may
342-- then be @{receive|received}.
343--
344-- This is automatically started in the background on computer startup, and
345-- should not be called manually.
346function run()
347 if bRunning then
348 error("rednet is already running", 2)
349 end
350 bRunning = true
351
352 while bRunning do
353 local sEvent, p1, p2, p3, p4 = os.pullEventRaw()
354 if sEvent == "modem_message" then
355 -- Got a modem message, process it and add it to the rednet event queue
356 local sModem, nChannel, nReplyChannel, tMessage = p1, p2, p3, p4
357 if isOpen(sModem) and (nChannel == CHANNEL_REPEAT or nChannel == CHANNEL_BROADCAST) then
358 if type(tMessage) == "table" and tMessage.nMessageID then
359 if not tReceivedMessages[tMessage.nMessageID] then
360 if tMessage.nRecipient == os.getComputerID() or tMessage.nRecipient == CHANNEL_BROADCAST then
361 tReceivedMessages[tMessage.nMessageID] = true
362 tReceivedMessageTimeouts[os.startTimer(30)] = tMessage.nMessageID
363 os.queueEvent("rednet_message", tMessage.nReplyChannel, tMessage.message, tMessage.sProtocol)
364 end
365 end
366 end
367 end
368
369 elseif sEvent == "rednet_message" then
370 -- Got a rednet message (queued from above), respond to dns lookup
371 local nSenderID, tMessage, sProtocol = p1, p2, p3
372 if sProtocol == "dns" and type(tMessage) == "table" and tMessage.sType == "lookup" then
373 local sHostname = tHostnames[tMessage.sProtocol]
374 if sHostname ~= nil and (tMessage.sHostname == nil or tMessage.sHostname == sHostname) then
375 rednet.send(nSenderID, {
376 sType = "lookup response",
377 sHostname = sHostname,
378 sProtocol = tMessage.sProtocol,
379 }, "dns")
380 end
381 end
382
383 elseif sEvent == "timer" then
384 -- Got a timer event, use it to clear the event queue
385 local nTimer = p1
386 local nMessage = tReceivedMessageTimeouts[nTimer]
387 if nMessage then
388 tReceivedMessageTimeouts[nTimer] = nil
389 tReceivedMessages[nMessage] = nil
390 end
391 end
392 end
393end
394