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