First attempt at adding support for the RMT device. (#3493)
* Adding the first version of the rmt documentation. * Stub RMT module compiles. * This version seems to work in (at least) simple cases. * CLean up the docs * Minor fixes * Give the SPI module a chance of working... * Update to the released version of idf4.4 * Try to get the CI Build to work in all cases * Try to get the CI Build to work in all cases * FIx a ringbuffer return issue * Remove bogus comment * Review comments * Better example of transmission * Review comments * Add table send example * Improved documentation * Documentation comments * Install the driver correctly. * A couple of doc updates * Fix typo
This commit is contained in:
parent
cb434811ca
commit
ceb62993da
|
@ -51,6 +51,7 @@ if(IDF_TARGET STREQUAL "esp32")
|
|||
"eth.c"
|
||||
"i2s.c"
|
||||
"pulsecnt.c"
|
||||
"rmt.c"
|
||||
"sdmmc.c"
|
||||
"touch.c"
|
||||
)
|
||||
|
|
|
@ -218,6 +218,13 @@ menu "NodeMCU modules"
|
|||
Includes the QR Code Generator from
|
||||
https://www.nayuki.io/page/qr-code-generator-library
|
||||
|
||||
config NODEMCU_CMODULE_RMT
|
||||
bool "Remote Control pulse generator/receiver"
|
||||
default "n"
|
||||
help
|
||||
Includes the rmt module to use the ESP32's built-in
|
||||
remote control hardware.
|
||||
|
||||
config NODEMCU_CMODULE_SDMMC
|
||||
depends on IDF_TARGET_ESP32
|
||||
bool "SD-MMC module"
|
||||
|
|
|
@ -0,0 +1,381 @@
|
|||
// Module for working with the rmt driver
|
||||
|
||||
#include <string.h>
|
||||
#include "module.h"
|
||||
#include "lauxlib.h"
|
||||
#include "platform.h"
|
||||
#include "platform_rmt.h"
|
||||
#include "task/task.h"
|
||||
|
||||
#include "driver/rmt.h"
|
||||
|
||||
#include "common.h"
|
||||
|
||||
typedef struct {
|
||||
bool tx;
|
||||
int channel;
|
||||
int cb_ref;
|
||||
struct _lrmt_cb_params *rx_params;
|
||||
} *lrmt_channel_t;
|
||||
|
||||
typedef struct _lrmt_cb_params {
|
||||
bool dont_call;
|
||||
bool rx_shutting_down;
|
||||
rmt_channel_t channel;
|
||||
int cb_ref;
|
||||
int data_ref;
|
||||
int rc;
|
||||
size_t len;
|
||||
rmt_item32_t *data;
|
||||
} lrmt_cb_params;
|
||||
|
||||
static task_handle_t cb_task_id;
|
||||
|
||||
static int get_divisor(lua_State *L, int index) {
|
||||
int bittime = luaL_checkinteger(L, index);
|
||||
|
||||
int divisor = bittime / 12500; // 80MHz clock
|
||||
|
||||
luaL_argcheck(L, divisor >= 1 && divisor <= 255, index, "Bit time out of range");
|
||||
|
||||
return divisor;
|
||||
}
|
||||
|
||||
static int configure_channel(lua_State *L, rmt_config_t *config, rmt_mode_t mode) {
|
||||
lrmt_channel_t ud = (lrmt_channel_t)lua_newuserdata(L, sizeof(*ud));
|
||||
if (!ud) return luaL_error(L, "not enough memory");
|
||||
memset(ud, 0, sizeof(*ud));
|
||||
luaL_getmetatable(L, "rmt.channel");
|
||||
lua_setmetatable(L, -2);
|
||||
|
||||
// We have allocated the channel -- must free it if the rest of this method fails
|
||||
int channel = platform_rmt_allocate(1, mode);
|
||||
|
||||
if (channel < 0) {
|
||||
return luaL_error(L, "no spare RMT channel");
|
||||
}
|
||||
|
||||
config->channel = channel;
|
||||
|
||||
ud->channel = channel;
|
||||
ud->tx = mode == RMT_MODE_TX;
|
||||
|
||||
esp_err_t rc = rmt_config(config);
|
||||
if (rc) {
|
||||
platform_rmt_release(config->channel);
|
||||
return luaL_error(L, "Failed to configure RMT");
|
||||
}
|
||||
|
||||
rc = rmt_driver_install(config->channel, 1000, 0);
|
||||
if (rc) {
|
||||
platform_rmt_release(config->channel);
|
||||
return luaL_error(L, "Failed to install RMT driver");
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int lrmt_txsetup(lua_State *L) {
|
||||
int gpio = luaL_checkinteger(L, 1);
|
||||
int divisor = get_divisor(L, 2);
|
||||
|
||||
// We will set the channel later
|
||||
rmt_config_t config = RMT_DEFAULT_CONFIG_TX(gpio, 0);
|
||||
config.clk_div = divisor;
|
||||
|
||||
if (lua_type(L, 3) == LUA_TTABLE) {
|
||||
lua_getfield(L, 3, "carrier_hz");
|
||||
int hz = lua_tointeger(L, -1);
|
||||
if (hz) {
|
||||
config.tx_config.carrier_freq_hz = hz;
|
||||
config.tx_config.carrier_en = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
lua_getfield(L, 3, "carrier_duty");
|
||||
int duty = lua_tointeger(L, -1);
|
||||
if (duty) {
|
||||
config.tx_config.carrier_duty_percent = duty;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
lua_getfield(L, 3, "idle_level");
|
||||
if (!lua_isnil(L, -1)) {
|
||||
int level = lua_tointeger(L, -1);
|
||||
config.tx_config.idle_level = level;
|
||||
config.tx_config.idle_output_en = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
lua_getfield(L, 3, "invert");
|
||||
if (lua_toboolean(L, -1)) {
|
||||
config.flags |= RMT_CHANNEL_FLAGS_INVERT_SIG;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
configure_channel(L, &config, RMT_MODE_TX);
|
||||
lua_pushinteger(L, divisor * 12500);
|
||||
return 2;
|
||||
}
|
||||
|
||||
static int lrmt_rxsetup(lua_State *L) {
|
||||
int gpio = luaL_checkinteger(L, 1);
|
||||
int divisor = get_divisor(L, 2);
|
||||
|
||||
// We will set the channel later
|
||||
rmt_config_t config = RMT_DEFAULT_CONFIG_RX(gpio, 0);
|
||||
config.clk_div = divisor;
|
||||
config.rx_config.idle_threshold = 65535;
|
||||
|
||||
if (lua_type(L, 3) == LUA_TTABLE) {
|
||||
lua_getfield(L, 3, "invert");
|
||||
if (lua_toboolean(L, -1)) {
|
||||
config.flags |= RMT_CHANNEL_FLAGS_INVERT_SIG;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
lua_getfield(L, 3, "filter_ticks");
|
||||
if (!lua_isnil(L, -1)) {
|
||||
int ticks = lua_tointeger(L, -1);
|
||||
if (ticks < 0 || ticks > 255) {
|
||||
return luaL_error(L, "filter_ticks must be in the range 0 - 255");
|
||||
}
|
||||
config.rx_config.filter_ticks_thresh = ticks;
|
||||
config.rx_config.filter_en = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
lua_getfield(L, 3, "idle_threshold");
|
||||
if (!lua_isnil(L, -1)) {
|
||||
int threshold = lua_tointeger(L, -1);
|
||||
if (threshold < 0 || threshold > 65535) {
|
||||
return luaL_error(L, "idle_threshold must be in the range 0 - 65535");
|
||||
}
|
||||
config.rx_config.idle_threshold = threshold;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
configure_channel(L, &config, RMT_MODE_RX);
|
||||
lua_pushinteger(L, divisor * 12500);
|
||||
return 2;
|
||||
}
|
||||
|
||||
static void free_transmit_wait_params(lua_State *L, lrmt_cb_params *p) {
|
||||
if (!p->data) {
|
||||
luaL_unref(L, LUA_REGISTRYINDEX, p->cb_ref);
|
||||
luaL_unref(L, LUA_REGISTRYINDEX, p->data_ref);
|
||||
}
|
||||
free(p);
|
||||
}
|
||||
|
||||
static void handle_receive(void *param) {
|
||||
lrmt_cb_params *p = (lrmt_cb_params *) param;
|
||||
|
||||
RingbufHandle_t rb = NULL;
|
||||
|
||||
//get RMT RX ringbuffer
|
||||
rmt_get_ringbuf_handle(p->channel, &rb);
|
||||
// Start receive
|
||||
rmt_rx_start(p->channel, true);
|
||||
while (!p->rx_shutting_down) {
|
||||
size_t length = 0;
|
||||
rmt_item32_t *items = NULL;
|
||||
|
||||
items = (rmt_item32_t *) xRingbufferReceive(rb, &length, 50 / portTICK_PERIOD_MS);
|
||||
if (items && length) {
|
||||
lrmt_cb_params *rx_params = malloc(sizeof(lrmt_cb_params) + length);
|
||||
if (rx_params) {
|
||||
memset(rx_params, 0, sizeof(*rx_params));
|
||||
memcpy(rx_params + 1, items, length);
|
||||
rx_params->cb_ref = p->cb_ref;
|
||||
rx_params->data = (void *) (rx_params + 1);
|
||||
rx_params->len = length;
|
||||
rx_params->channel = p->channel;
|
||||
task_post_high(cb_task_id, (task_param_t) rx_params);
|
||||
} else {
|
||||
printf("Unable allocate receive data memory\n");
|
||||
}
|
||||
}
|
||||
if (items) {
|
||||
vRingbufferReturnItem(rb, (void *) items);
|
||||
}
|
||||
}
|
||||
|
||||
p->dont_call = true;
|
||||
task_post_high(cb_task_id, (task_param_t) p);
|
||||
|
||||
/* Destroy this task */
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
static int lrmt_on(lua_State *L) {
|
||||
lrmt_channel_t ud = (lrmt_channel_t)luaL_checkudata(L, 1, "rmt.channel");
|
||||
if (ud->tx) {
|
||||
return luaL_error(L, "Cannot receive on a TX channel");
|
||||
}
|
||||
|
||||
luaL_argcheck(L, !strcmp(lua_tostring(L, 2), "data") , 2, "Must be 'data'");
|
||||
|
||||
luaL_argcheck(L, lua_type(L, 3) == LUA_TFUNCTION, 3, "Must be a function");
|
||||
|
||||
if (ud->rx_params) {
|
||||
return luaL_error(L, "Can only call 'on' once");
|
||||
}
|
||||
|
||||
// We have a callback
|
||||
lrmt_cb_params *params = (lrmt_cb_params *) malloc(sizeof(*params));
|
||||
if (!params) {
|
||||
return luaL_error(L, "Cannot allocate memory");
|
||||
}
|
||||
memset(params, 0, sizeof(*params));
|
||||
|
||||
lua_pushvalue(L, 3);
|
||||
params->cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||
ud->rx_params = params;
|
||||
params->channel = ud->channel;
|
||||
|
||||
xTaskCreate(handle_receive, "rmt-rx-receiver", 3000, params, 2, NULL);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void wait_for_transmit(void *param) {
|
||||
lrmt_cb_params *p = (lrmt_cb_params *) param;
|
||||
esp_err_t rc = rmt_wait_tx_done(p->channel, 10000 / portTICK_PERIOD_MS);
|
||||
|
||||
p->rc = rc;
|
||||
task_post_high(cb_task_id, (task_param_t) p);
|
||||
|
||||
/* Destroy this task */
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
static int lrmt_send(lua_State *L) {
|
||||
lrmt_channel_t ud = (lrmt_channel_t)luaL_checkudata(L, 1, "rmt.channel");
|
||||
if (!ud->tx) {
|
||||
return luaL_error(L, "Cannot send on an RX channel");
|
||||
}
|
||||
int string_index = 2;
|
||||
|
||||
if (lua_type(L, 2) == LUA_TTABLE) {
|
||||
lua_getfield(L, 2, "concat");
|
||||
lua_pushvalue(L, 2);
|
||||
lua_pushstring(L, "");
|
||||
lua_call(L, 2, 1);
|
||||
string_index = -1;
|
||||
}
|
||||
|
||||
size_t len;
|
||||
const char *data = lua_tolstring(L, string_index, &len);
|
||||
if (!data || !len) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (len & 1) {
|
||||
return luaL_error(L, "Length must be a multiple of 2");
|
||||
}
|
||||
|
||||
if (len & 3) {
|
||||
// Just tack on a "\0\0" -- this is needed as the hardware can
|
||||
// only deal with multiple of 4 bytes.
|
||||
luaL_Buffer b;
|
||||
luaL_buffinit(L, &b);
|
||||
luaL_addlstring(&b, data, len);
|
||||
luaL_addlstring(&b, "\0\0", 2);
|
||||
luaL_pushresult(&b);
|
||||
data = lua_tolstring(L, -1, &len);
|
||||
string_index = -1;
|
||||
}
|
||||
|
||||
bool wait_for_done = true;
|
||||
|
||||
if (lua_type(L, 3) == LUA_TFUNCTION) {
|
||||
// We have a callback
|
||||
lrmt_cb_params *params = (lrmt_cb_params *) malloc(sizeof(*params));
|
||||
if (!params) {
|
||||
return luaL_error(L, "Cannot allocate memory");
|
||||
}
|
||||
memset(params, 0, sizeof(*params));
|
||||
|
||||
params->channel = ud->channel;
|
||||
|
||||
lua_pushvalue(L, 3);
|
||||
params->cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||
lua_pushvalue(L, string_index);
|
||||
params->data_ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||
xTaskCreate(wait_for_transmit, "rmt-tx-waiter", 1024, params, 2, NULL);
|
||||
wait_for_done = false;
|
||||
}
|
||||
|
||||
// We want to transmit it
|
||||
rmt_write_items(ud->channel, (rmt_item32_t *) data, len / sizeof(rmt_item32_t), wait_for_done);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int lrmt_close(lua_State *L) {
|
||||
lrmt_channel_t ud = (lrmt_channel_t)luaL_checkudata(L, 1, "rmt.channel");
|
||||
|
||||
if (ud->channel >= 0) {
|
||||
if (ud->rx_params) {
|
||||
// We need to stop the listening task
|
||||
ud->rx_params->rx_shutting_down = true;
|
||||
} else {
|
||||
rmt_driver_uninstall(ud->channel);
|
||||
platform_rmt_release(ud->channel);
|
||||
}
|
||||
ud->channel = -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void cb_task(task_param_t param, task_prio_t prio) {
|
||||
lrmt_cb_params *p = (lrmt_cb_params *) param;
|
||||
lua_State *L = lua_getstate();
|
||||
|
||||
if (!p->dont_call) {
|
||||
lua_rawgeti (L, LUA_REGISTRYINDEX, p->cb_ref);
|
||||
if (p->data) {
|
||||
lua_pushlstring(L, (char *) p->data, p->len);
|
||||
} else {
|
||||
lua_pushinteger(L, p->rc);
|
||||
}
|
||||
|
||||
int res = luaL_pcallx(L, 1, 0);
|
||||
if (res) {
|
||||
printf("rmt callback threw an error\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (p->rx_shutting_down) {
|
||||
rmt_driver_uninstall(p->channel);
|
||||
platform_rmt_release(p->channel);
|
||||
}
|
||||
|
||||
free_transmit_wait_params(L, p);
|
||||
}
|
||||
|
||||
// Module function map
|
||||
LROT_BEGIN(rmt_channel, NULL, LROT_MASK_GC_INDEX)
|
||||
LROT_FUNCENTRY( __gc, lrmt_close )
|
||||
LROT_TABENTRY ( __index, rmt_channel )
|
||||
LROT_FUNCENTRY( on, lrmt_on )
|
||||
LROT_FUNCENTRY( close, lrmt_close )
|
||||
LROT_FUNCENTRY( send, lrmt_send )
|
||||
LROT_END(rmt_channel, NULL, LROT_MASK_GC_INDEX)
|
||||
|
||||
LROT_BEGIN(rmt, NULL, LROT_MASK_INDEX)
|
||||
LROT_TABENTRY ( __index, rmt )
|
||||
LROT_FUNCENTRY( txsetup, lrmt_txsetup )
|
||||
LROT_FUNCENTRY( rxsetup, lrmt_rxsetup )
|
||||
LROT_END(rmt, NULL, LROT_MASK_INDEX)
|
||||
|
||||
int luaopen_rmt(lua_State *L) {
|
||||
luaL_rometatable(L, "rmt.channel", LROT_TABLEREF(rmt_channel)); // create metatable
|
||||
cb_task_id = task_get_id(cb_task);
|
||||
return 0;
|
||||
}
|
||||
|
||||
NODEMCU_MODULE(RMT, "rmt", rmt, luaopen_rmt);
|
|
@ -0,0 +1,139 @@
|
|||
# Remote Control Driver
|
||||
| Since | Origin / Contributor | Maintainer | Source |
|
||||
| :----- | :-------------------- | :---------- | :------ |
|
||||
| 2022-01-01 | [pjsg](https://github.com/pjsg) | [pjsg](https://github.com/pjsg) | [rmt.c](../../components/modules/rmt.c)|
|
||||
|
||||
The RMT module provides a simple interface onto the ESP32 RMT peripheral. This allows the generation of
|
||||
arbitrary pulse trains with good timing accuracy. This can be used to generate IR remote control signals, or
|
||||
servo control pulses, or pretty much any high speed signalling system. It isn't good for low speed stuff as the maximum
|
||||
pulse time is under 200ms -- though you can get longer by having multiple large values in a row. See the Data Encoding
|
||||
below for more details.
|
||||
|
||||
## rmt.txsetup(gpio, bitrate, options)
|
||||
|
||||
This sets up a transmit channel on the specified gpio pin at the specified rate. Various options described below
|
||||
can be specified in the `options`. The bit time is specified in picoseconds so that integer values can be used.
|
||||
|
||||
An error will be thrown if the bit time cannot be approximated.
|
||||
|
||||
#### Syntax
|
||||
`channel = rmt.txsetup(gpio, bittime[, options])`
|
||||
|
||||
#### Parameters
|
||||
- `gpio` The GPIO pin number to use.
|
||||
- `bittime` The bit time to use in picoseconds. Only certain times can be handled exactly. The actual set time will be returned. The actual range is limited -- probably using 100,000 (0.1µS) or 1,000,000 (1µS). The actual constraint is that the interval is 1 - 255 cycles of an 80MHz clock.
|
||||
- `options` A table with the keys as defined below.
|
||||
|
||||
##### Returns
|
||||
- The `rmt.channel` object that can be used for sending data
|
||||
- The actual bit time in picoseconds.
|
||||
|
||||
#### Example
|
||||
```lua
|
||||
```
|
||||
|
||||
#### Options table
|
||||
|
||||
This optional table consists of a number of keys that control various aspects of the RMT transmission.
|
||||
|
||||
- `invert` if true, then the output is inverted.
|
||||
- `carrier_hz` specifies that the signal is to modulate the carrier at the specified frequency. This is useful for IR transmissions.
|
||||
- `carrier_duty` specifies the duty cycle of the carrier. Defaults to 50%
|
||||
- `idle_level` specifies what value to send when the transmission completes.
|
||||
|
||||
## rmt.rxsetup(gpio, bitrate, options)
|
||||
|
||||
This sets up a receive channel on the specified gpio pin at the specified rate. Various options described below
|
||||
can be specified in the `options`. The bit time is specified in picoseconds so that integer values can be used.
|
||||
|
||||
An error will be thrown if the bit time cannot be approximated.
|
||||
|
||||
#### Syntax
|
||||
`channel = rmt.rxsetup(gpio, bittime[, options])`
|
||||
|
||||
#### Parameters
|
||||
- `gpio` The GPIO pin number to use.
|
||||
- `bittime` The bit time to use in picoseconds. Only certain times can be handled exactly. The actual set time will be returned. The actual range is limited -- probably using 100,000 (0.1µS) or 1,000,000 (1µS). The actual constraint is that the interval is 1 - 255 cycles of an 80MHz clock.
|
||||
- `options` A table with the keys as defined below.
|
||||
|
||||
##### Returns
|
||||
- The `rmt.channel` object that can be used for receiving data
|
||||
- The actual bit time in picoseconds.
|
||||
|
||||
#### Example
|
||||
```lua
|
||||
```
|
||||
|
||||
#### Options table
|
||||
|
||||
This optional table consists of a number of keys that control various aspects of the RMT transmission.
|
||||
|
||||
- `invert` if true, then the input is inverted.
|
||||
- `filter_ticks` If specified, then any pulse shorter than this will be ignored. This is in units of the bit time.
|
||||
- `idle_threshold` If specified, then any level longer than this will set the receiver as idle. The default is 65535 bit times.
|
||||
|
||||
|
||||
## channel:on(event, callback)
|
||||
|
||||
This is establishes a callback to use when data is received and it also starts the data reception process. It can only be called once per receive
|
||||
channel.
|
||||
|
||||
#### Syntax
|
||||
`channel:on(event, callback)`
|
||||
|
||||
#### Parameters
|
||||
- `event` This must be the string 'data' and it sets the callback that gets invoked when data is received.
|
||||
- `callback` This is invoked with a single argument that is a string that contains the data received in the format described for `send` below. `struct.unpack` is your friend.
|
||||
|
||||
#### Returns
|
||||
`nil`
|
||||
|
||||
## channel:send(data, cb)
|
||||
|
||||
This is a (default) blocking call that transmits the data using the parameters specified on the `txsetup` call.
|
||||
|
||||
#### Syntax
|
||||
`channel:send(data[, cb])`
|
||||
|
||||
#### Parameters
|
||||
- `data` This is either a string or a table of strings.
|
||||
- `cb` This is an optional callback when the transmission is actually complete. If specified, then the `send` call is non-blocking, and the callback invoked when the transmission is complete. Otherwise the `send` call is synchronous and does not return until transmission is complete.
|
||||
|
||||
#### Data Encoding
|
||||
|
||||
If the `data` supplied is a table (really an array), then the elements of the table are concatenated together and sent. The elements of the table must be strings.
|
||||
|
||||
If the item being sent is a string, then it contains 16 bit packed integers. The top bit of the integer controls the output level.
|
||||
`struct.pack("H", value)` generates a suitable value to output a zero bit. `struct.pack("H", 32768 + value)` generates a one bit of the specified width.
|
||||
|
||||
The widths are in units of the interval specified when the channel was setup.
|
||||
|
||||
|
||||
#### Returns
|
||||
`nil`
|
||||
|
||||
#### Example
|
||||
|
||||
This example sends a single R character at 19200 bps. You wouldn't actually want to do it this way.... In some applications this would be inverted.
|
||||
|
||||
```
|
||||
channel = rmt.txsetup(25, 1000000000 / 19200, {idle_level=1})
|
||||
one = struct.pack("H", 32768 + 1000)
|
||||
zero = struct.pack("H", 1000)
|
||||
-- Send start bit, then R = 0x52 (reversed) then stop bit
|
||||
channel:send(zero .. zero .. one .. zero .. zero .. one .. zero .. one .. zero .. one)
|
||||
-- or using the table interface
|
||||
channel:send({zero, zero, one, zero, zero, one, zero, one, zero, one})
|
||||
```
|
||||
|
||||
## channel:close()
|
||||
|
||||
This shuts down the RMT channel and makes it available for other uses (e.g. ws2812). The channel cannot be used after this call returns. The channel
|
||||
is also released when the garbage collector frees it up. However you should always `close` the channel explicitly as otherwise you can run out of RMT channels
|
||||
before the garbage collector frees some up.
|
||||
|
||||
#### Syntax
|
||||
`channel:close()`
|
||||
|
||||
#### Returns
|
||||
`nil`
|
Loading…
Reference in New Issue