通过 Slack 与您的 rPi 聊天





5.00/5 (25投票s)
通过 Slack 频道与您的 rPi 通信,获取状态,控制设备,并运行 shell (bash) 命令,同时将控制台输出发布回您的 Slack 频道。
目录
前言
本文是我提交给 Slack API 挑战的作品。
引言
您的 rPi 放在您的本地网络上,您(希望!)没有在路由器上打开 telnet 端口,您不在家,但您需要与您的 rPi 通信。一个可怜的男孩该怎么办?嗯,如果您已将此 Slack Bot 应用添加到您的 Slack 帐户,您可以从任何地方与您的 rPi 通信!发送 bash 命令,检查其运行状况,控制您的设备,一切尽在 Slack!
示例
简单的 Ping
获取您的 rPi 处理器温度
获取内存利用率 (Bash 命令)
获取可用磁盘空间 (Bash 命令)
向设备发送命令,例如 LCD1602
检查进程 (Bash 命令)
查询 Postgres
您应该已经知道的内容
您应该已经知道
- 如何创建 .NET Core 应用程序
- 以 linux-arm 架构发布它
- 使用 WinSCP (或 PCSP) 将其传输到 rPi
- 在 Putty 中启动一个终端窗口来运行它。
如果您不熟悉这些步骤,可以通过谷歌搜索“rpi dotnet core”找到大量资源,或者参考我的上一篇文章,特别是关于 PuTTY和WinSCP、安装.NET Core、在 rPi 上运行测试的部分。如果您不熟悉 LCD1602,请参考我的文章。
如果我没有 rPi 怎么办?
如果您没有 rPi,您仍然可以在 Visual Studio 中运行该应用程序,当然 Linux、rPi 和 LCD1602 的特定功能将无法正常工作。 这样您只能发送“ping”命令,但您可以轻松地添加其他功能。 事实上,我用 Visual Studio 在 Windows 机器上完成了许多 Slack API 的测试。
在 Slack 中创建 Bot 应用
第一步是为您的 Slack 帐户创建一个 bot 应用。 如果您还没有 Slack 帐户,请按照 Ryan Peden 的“创建您的第一个 Slack”文章中的“如何入门”部分进行操作。重要提示!在创建应用程序时,您还需要创建一个 bot。例如,我的 bot 名为“rpichat
”,并且列在https://api.slack.com/apps页面上。
点击 Bot,您将看到这个
点击“添加功能和功能”,您将看到这个
点击 Bots
然后点击“添加 Bot 用户”。设置显示名称和默认用户名,然后点击添加 Bot 用户。
在左侧点击 OAuth & Permissions。
如果您尚未在您的工作区中安装该应用,您将看到此按钮。
安装该应用,授权它,现在您就可以看到 Bot 用户 OAuth 访问令牌了。
代码
首先,需要将三个包添加到项目中。
Main
这非常简单。
private static SlackSocketClient client;
static void Main(string[] args)
{
Console.WriteLine("Initializing...");
InitializeSlack();
Console.WriteLine("Slack Ready.");
Console.WriteLine("Press ENTER to exit.");
Console.ReadLine();
}
SlackAPI
对于本文,我使用的是SlackAPI,一个开源的 C# .NET Standard 库,它在 rPi 上运行得很好。请注意,我尚未调查其在 WebSocket 连接丢失和恢复方面的健壮性。
因为我们使用的是实时消息 (RTM) API 和 bot 应用,所以我们需要 Bot 用户 OAuth 访问令牌,可以在 https://api.slack.com/apps页面上找到(然后导航到您的应用)。这是一个需要记住的重要链接,它是您所有应用的入口。在 OAuth 访问部分,您应该会看到类似这样的内容,当然,令牌会被隐藏起来。
将 Bot Token(以及如果您需要,也可以是 OAuth Token)复制到 *appSettings.json* 文件中。
{
"Slack": {
"AccessToken": "[you access token]",
"BotToken": "your bot token]"
}
}
本文未使用“AccessToken
”,但您可能希望将其保留以备将来使用。
初始化 API 并接收消息
使用 API 非常直接。 一个方法处理启动和消息路由,注释和代码(我稍作修改)来自SlackAPI wiki 页面示例。
static void InitializeSlack()
{ string botToken = Configuration["Slack:BotToken"];
ManualResetEventSlim clientReady = new ManualResetEventSlim(false);
client = new SlackSocketClient(botToken);
client.Connect((connected) => {
// This is called once the client has emitted the RTM start command
clientReady.Set();
}, () => {
// This is called once the RTM client has connected to the end point
});
client.OnMessageReceived += (message) =>
{
// Handle each message as you receive them
Console.WriteLine(message.user + "(" + message.username + "): " + message.text);
if (message.text.StartsWith("rpi:"))
{
// Skip any spaces after "rpi:" and get what's left of the first space, ignoring data.
string cmd = message.text.RightOf("rpi:").Trim().LeftOf(" ");
// Get everything to the right after the command and any number of spaces
// separating the start of the data.
string data = message.text.RightOf("rpi:").Trim().RightOf(" ").Trim();
Console.WriteLine("cmd: " + cmd);
Console.WriteLine("data: " + data);
string ret = "Error occurred.";
if (router.TryGetValue(cmd, out Func<string, string> fnc))
{
ret = fnc(data);
}
else
{
// Try as bash command.
string cmdline = message.text.RightOf("rpi:").Trim();
ret = "```" + cmdline.Bash() + "```";
}
client.PostMessage((mr) => { }, message.channel, ret);
}
};
clientReady.Wait();
}
关于上面代码中的消息处理程序有三点需要注意:
- 任何不以“
rpi:
”开头的消息都将被忽略。这是因为当应用程序发布消息时,会触发消息事件,因此应用程序会接收到它刚刚发布的消息。为了区分您用户发送的命令,您的命令必须以“rpi:
”为前缀。 - bash 命令的控制台输出将以 markdown 块引用形式返回,使用等宽字体并保留前导空格,因此您可以获得格式良好的结果。
- 我们始终在接收消息的频道中进行回复。您可能正在 Bot 的直接消息频道中与其聊天,或者如果 Bot 已被邀请到一个“人类”频道,我们也可以在那里与其聊天。
设置配置解析器
配置解析器需要 `using Microsoft.Extensions.Configuration;`,并实现为 `static` getter(从此处借用),以及前面提到的两个 NuGet 包。
public static IConfiguration Configuration { get; } = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appSettings.json", optional: false, reloadOnChange: true)
.Build();
在 *nix 系统上,文件名区分大小写,所以请务必保留文件名的大小写以及在上述代码中引用它的方式。
命令路由器
路由器只是一个命令键 - 函数字典 -- 如果一个命令有一个关联的函数,那么这个函数就会被调用,否则就假定它是一个 bash 命令,然后进程会被调用。
private static Dictionary<string, Func<string, string>> router =
new Dictionary<string, Func<string, string>>
{
{"temp", GetTemp },
{"display", Display },
{"ping", (_) => "pong" },
};
扩展此功能以实现自定义 C# 实现。
Bash 进程调用器
这段代码是从这里借用的。
public static string Bash(this string cmd)
{
var escapedArgs = cmd.Replace("\"", "\\\"");
var process = new Process()
{
StartInfo = new ProcessStartInfo
{
FileName = "/bin/bash",
Arguments = $"-c \"{escapedArgs}\"",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
}
};
process.Start();
string result = process.StandardOutput.ReadToEnd();
process.WaitForExit();
return result;
}
它实现了一个扩展方法,因此使用方式看起来像 `"df -h".Bash()`。参数中的 `-c` 实际上是告诉 bash 要运行的命令(作为 bash 的参数)。
我实现的命令
除了“ping
”命令,我还根据我目前连接的一些东西实现了其他一些功能。
获取 rPi 温度
处理器有一个内置的温度传感器,温度存储在一个文件中(我最初在一些 Python 代码中找到它,并添加了华氏度计算)。
private static string GetTemp(string _)
{
string ret;
string temp = System.IO.File.ReadAllText("/sys/class/thermal/thermal_zone0/temp");
Console.WriteLine(temp);
double t = Convert.ToDouble(temp);
string dc = String.Format("{0:N2}", t / 1000);
string df = String.Format("{0:N2}", t / 1000 * 9 / 5 + 32);
ret = dc + "C" + " " + df + "F";
return ret;
}
写入 LCD1602
从我的上一篇文章中,我现在可以从 Slack 向 `LCD1602` 显示消息了!
// Data must be in the format of one of these two options:
// "This is line 1"
// "This is line 1"[d]"This is line 2"
// where [d] is an optional delimiter of any string.
private static string Display(string data)
{
int numQuotes = data.Count(c => c == '\"');
if (data.First() != '\"' || data.Last() != '\"' && (numQuotes != 2 && numQuotes != 4))
{
return "bad format";
}
Lcd1602 lcd = new Lcd1602();
lcd.OpenDevice("/dev/i2c-1", LCD1602_ADDRESS);
lcd.Init();
lcd.Clear();
if (numQuotes == 2)
{
lcd.Write(0, 0, data.Between("\"", "\""));
}
else
{
// two lines
lcd.Write(0, 0, data.Between("\"", "\""));
lcd.Write(0, 1, data.RightOf("\"").RightOf("\"").Between("\"", "\""));
}
lcd.CloseDevice();
return "ok";
}
查询 Postgres
实际上,任何 Postgres SQL 命令都可以通过您的 Slack bot 执行,这里我展示的是查询。
执行 SQL,包括查询,使用 ADO.NET 非常直接。对于查询,可以提供格式化选项(默认为 JSON)。`Match` 扩展方法记录在我的文章中。我的 Match 扩展方法可能会被 C# 8 的 `switch` 语句及其出色的简洁性所取代。
Northwind
数据库已使用此 GitHub 仓库导入到 Postgres 中。
关于**fetch first 2 rows only**,这是SQL 2008的一部分,但在 MS SQL Server 中不起作用!
另外,要使此功能生效,请将适当的连接字符串添加到您的 *appsettings.json* 文件中。
"ConnectionStrings": {
"rpidb": "Host=[your IP];Database=Northwind;Username=pi;Password=[your password]"
}
`ExecuteSQL` 方法
enum OutputFormat
{
JSON,
CSV,
Tabular,
}
private static string ExecuteSql(string data, List<string> options)
{
string sql = data;
var outputFormat = OutputFormat.JSON;
string ret = "";
string validOptionsErrorMessage = "Valid options are --json, --csv, --tabular";
try
{
options.Match(
(o => o.Count == 0, _ => { }),
(o => o.Count > 1, _ => throw new Exception(validOptionsErrorMessage)),
(o => o[0] == "--json", _ => outputFormat = OutputFormat.JSON),
(o => o[0] == "--csv", _ => outputFormat = OutputFormat.CSV),
(o => o[0] == "--tabular", _ => outputFormat = OutputFormat.Tabular),
(_ => true, _ => throw new Exception(validOptionsErrorMessage))
);
string connStr = Configuration.GetValue<string>("ConnectionStrings:rpidb");
var conn = new NpgsqlConnection(connStr);
conn.Open();
var cmd = new NpgsqlCommand(sql, conn);
NpgsqlDataAdapter da = new NpgsqlDataAdapter(cmd);
DataTable dt = new DataTable();
da.Fill(dt);
ret = outputFormat.MatchReturn(
(f => f == OutputFormat.JSON, _ => Jsonify(dt)),
(f => f == OutputFormat.CSV, _ => Csvify(dt)),
(f => f == OutputFormat.Tabular, _ => Tabify(dt))
);
ret = "```\r\n" + ret + "```";
}
catch (Exception ex)
{
ret = ex.Message;
}
return ret;
}
返回 JSON
使用 *Newtsoft.JSON* 非常简单。
static string Jsonify(DataTable dt)
{
string ret = JsonConvert.SerializeObject(dt, Formatting.Indented);
return ret.ToString();
}
示例
返回 CSV
这也很简单。
static string Csvify(DataTable dt)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine(String.Join(", ", dt.Columns.Cast<DataColumn>().Select(dc => dc.ColumnName)));
foreach (DataRow row in dt.Rows)
{
sb.AppendLine(String.Join(", ", dt.Columns.Cast<DataColumn>().Select(dc => row[dc].ToString())));
}
return sb.ToString();
}
示例
返回表格格式数据
在这里,每个列名和行的宽度数据都考虑了,需要使用 `Math.Max` 来计算列名和每一行数据的宽度。`Tabify` 可能不是最好的名字!
static string Tabify(DataTable dt)
{
StringBuilder sb = new StringBuilder();
int[] colWidth = new int[dt.Columns.Count];
// Get max widths for each column.
dt.Columns.Cast<DataColumn>().ForEachWithIndex((dc, idx) =>
colWidth[idx] = Math.Max(colWidth[idx], dc.ColumnName.Length));
// Get the max width of each row's column.
dt.AsEnumerable().ForEach(r =>
{
dt.Columns.Cast<DataColumn>().ForEachWithIndex((dc, idx) =>
colWidth[idx] = Math.Max(colWidth[idx], r[dc].ToString().Length));
});
// Bump all widths by 3 for better visual separation
colWidth.ForEachWithIndex((n, idx) => colWidth[idx] = n + 3);
// Padded column names:
sb.AppendLine(string.Concat(dt.Columns.Cast<DataColumn>().Select((dc, idx) =>
dc.ColumnName.PadRight(colWidth[idx]))));
// Padded row data:
dt.AsEnumerable().ForEach(r =>
sb.AppendLine(string.Concat(dt.Columns.Cast<DataColumn>().Select((dc, idx) =>
r[dc].ToString().PadRight(colWidth[idx])))));
return sb.ToString();
}
示例
限制
Slack 频道的内容有 4000 个字符的限制,所以不要疯狂地查询数百条记录和几十列!
让程序保持运行
如果您想将应用程序设置为服务,以便在断电时自动启动,例如,请参阅我关于设置服务的上一篇文章。或者,如果您只是想让程序在关闭终端窗口后仍然保持运行。
nohup > /dev/null &
`&` 使进程在后台运行(对您启动的任何进程都是如此),而 `nohup` 是“no hang up”(不挂起),即在终端关闭时不会中断。重定向 `> /dev/null` 将控制台输出重定向到空,而不是 `nohup.out` 文件。 在此处阅读有关 nohup 的信息。
在这种情况下,我更改了 `main` 函数,删除了“**按 ENTER 退出**”,并用一个空循环替换它。
while (!stop) Thread.Sleep(1);
Thread.Sleep(1000); // wait for final message to be sent.
// Console.WriteLine("Press ENTER to exit.");
//Console.ReadLine();
我添加了一个“stop
”命令,以便从 Slack 终止程序。
{"stop", _ => {stop=true; return "Stopping..."; } }
结论
虽然代码非常简单,但这开启了 Slack 和 rPi(或任何 SBC)之间双向通信的全新世界。我可以想象用它来执行诸如启动和停止服务、获取连接到 rPi 的各种设备的状态、发出命令等等操作。我可以想象将一个 rPi 或 Arduino 安装在乐高机器人上,并使用 Slack 消息来驱动机器人,甚至发布图像文件!如果我有时间和硬件,我肯定会进一步探索。
修订历史
- 2019年1月27日 - 添加了 Postgres 命令处理。