PLC声光语音告警系统:基于Modbus TCP的工业现场多模态告警实现

PLC声光语音告警系统:基于Modbus TCP的工业现场多模态告警实现 1. 为什么PLC需要“开口说话”——从沉默的控制器到主动告警的现场哨兵在产线调试现场我见过太多次这样的场景设备突然停机操作工盯着HMI屏幕发愣维修人员拿着万用表满车间跑而PLC——这个本该最了解设备状态的“大脑”却像一块沉默的砖头只在寄存器里悄悄把故障码从0x00变成0x0A。没人知道它想说什么直到你翻出梯形图在第47个网络块里找到那个被注释掉的报警输出点。这不是技术缺陷而是人机交互的断层PLC擅长执行逻辑却不擅长表达意图它能精准控制毫秒级的电磁阀动作却无法在电机过载的瞬间用一句“主轴温度超限请立即停机”拦住正要按下启动键的操作员。这就是“让PLC开口说话”的真实起点——不是炫技而是补上工业现场最后一环的信息通路。Modbus TCP不是新协议但把它和声光语音告警组合起来就构成了一套低成本、高适配、零硬件改造的“现场语义层”。它不替换原有PLC不改动梯形图逻辑只是在PLC的Modbus寄存器池里“听诊”当某个保持寄存器比如40001的值从0跳变到1时Python程序立刻触发蜂鸣器、闪光灯并调用系统TTS引擎播报预设语音。整个过程绕开了PLC厂商专用软件的封闭生态也避开了OPC UA部署的证书与配置复杂度。我去年在一家汽车零部件厂落地这套方案时产线主管的第一反应是“原来不用换PLC也能让老设备‘会说话’”——这句话点破了本质这不是给PLC加功能而是给现场人员加一双耳朵、一双眼睛。关键词里的“声光语音告警”绝非简单叠加。声蜂鸣器频率/占空比、光LED颜色/闪烁模式、音语音内容/语速/音量三者必须形成可区分的告警等级一级故障如急停触发用1kHz连续蜂鸣红灯常亮语音“紧急停机”二级故障如气压不足用500Hz间歇蜂鸣黄灯快闪语音“气源压力偏低”三级提示如保养提醒用300Hz单次短鸣绿灯慢闪语音“润滑周期到期”。这种分级不是拍脑袋定的而是依据GB/T 2893-2013《安全色》和GB/T 2894-2008《安全标志及其使用导则》的强制要求。Python在这里扮演的是“翻译官”角色——把PLC寄存器里的十六进制数值翻译成人类可感知的多模态信号。它不碰PLC的控制逻辑只做信息解码与呈现这正是工业现场最需要的“轻量级介入”。2. Modbus TCP握手实录从信捷PLC寄存器映射到Python读取的完整链路调试的第一步永远不是写代码而是确认PLC是否真的在“说人话”。信捷XC系列PLC作为Modbus TCP服务器其寄存器地址映射规则与西门子、三菱存在本质差异——它不按传统“4x”、“0x”前缀划分而是直接以十进制偏移量定位。比如手册里写的“D100对应Modbus地址40101”实际意味着当你向PLC发送Modbus TCP请求读取寄存器40101时PLC返回的是D100的数据但若你想写入D100必须向地址40101写入而非40001。这个细节踩坑率接近100%因为绝大多数Modbus库包括pymodbus默认按标准地址计算而信捷的地址偏移量需手动减去1。我第一次调试时反复读取40101返回0直到用Wireshark抓包发现PLC响应报文里的数据域全是0x0000才意识到问题出在地址映射上。2.1 信捷PLC Modbus TCP服务启用与寄存器配置信捷PLC的Modbus TCP服务并非默认开启需通过XG-Studio软件进入“系统设置→通信设置→以太网设置”启用“Modbus TCP服务器”选项注意不是“Modbus RTU”设置监听端口为502标准端口若被占用可改但Python端必须同步关键一步在“Modbus地址映射”表中明确勾选D区数据寄存器的使能并设定起始地址。例如若希望D0-D999映射到Modbus地址40001-40999则需将“D区起始地址”设为0“Modbus起始地址”设为40001。这里极易混淆信捷的“D区起始地址”是PLC内部地址偏移而“Modbus起始地址”是对外暴露的协议地址。很多工程师误将D100填入“D区起始地址”导致实际映射错位。提示信捷PLC的D区寄存器为16位有符号整数但Modbus协议传输的是无符号格式。当PLC写入D100 -10xFFFF时Python读取到的原始值是65535。必须在Python端做符号转换value value if value 32768 else value - 65536。这个转换不能依赖pymodbus的自动处理因其默认按无符号解析。2.2 Python端Modbus TCP客户端构建与健壮性设计我选用pymodbus 3.6.0版本非旧版pymodbus3因其对异步支持更完善且修复了信捷PLC常见的“重复连接拒绝”问题。核心代码结构如下from pymodbus.client import ModbusTcpClient from pymodbus.exceptions import ModbusIOException, ConnectionException import time class PLCReader: def __init__(self, host192.168.1.10, port502, timeout2): self.client ModbusTcpClient(host, portport, timeouttimeout) self.connected False def connect(self): # 三次重试机制避免网络抖动导致失败 for i in range(3): try: if self.client.connect(): self.connected True print(fPLC连接成功: {self.client.host}:{self.client.port}) return True except Exception as e: print(f连接尝试{i1}失败: {e}) time.sleep(1) return False def read_holding_registers(self, address, count1): if not self.connected: return None try: # 信捷PLC地址需减1D100对应Modbus地址40101但pymodbus中address参数应传100 # 因为pymodbus内部已做40001处理我们只需传D区偏移量 result self.client.read_holding_registers(address, countcount, slave1) if result.isError(): print(f读取寄存器{address}失败: {result}) return None return result.registers except (ModbusIOException, ConnectionException) as e: print(fModbus读取异常: {e}) self.connected False # 标记断连下次调用connect重连 return None这段代码的关键设计在于地址偏移修正read_holding_registers(100, 1)直接读取D100无需手动计算40101。pymodbus的read_holding_registers方法内部已将address参数加上40001我们只需传PLC内部D区地址。连接状态管理self.connected标志位避免在断连状态下持续发送无效请求减少PLC通信负载。错误隔离捕获ModbusIOException网络层错误和ConnectionException连接层错误并主动置connectedFalse确保下次读取前强制重连。2.3 寄存器监控策略轮询 vs 中断工业现场的务实选择理论上基于事件的中断式监控更高效但Modbus TCP协议本身不支持“服务器主动推送”所有通信必须由客户端发起。因此轮询是唯一可行方案。但轮询频率绝非越高越好——信捷PLC的Modbus TCP服务最大并发连接数为4单连接最大请求频率为10次/秒。若Python程序以50ms间隔轮询10个寄存器相当于每秒200次请求必然触发PLC的连接拒绝或响应超时。我的实测经验是采用分级轮询策略关键告警寄存器如急停状态D0、过载标志D1100ms轮询一次10Hz满足实时性要求状态寄存器如运行模式D10、当前工序D11500ms轮询一次2Hz平衡响应与负载历史数据寄存器如累计运行时间D1005秒轮询一次0.2Hz避免无谓开销。这种策略通过threading.Timer实现而非简单的time.sleep()阻塞确保各通道独立计时互不干扰。更重要的是每次轮询后必须校验数据有效性若连续3次读取同一寄存器返回全0或异常值如65535则判定为通信异常触发本地告警并记录日志而非盲目等待PLC恢复。3. 声光语音告警的物理层实现从GPIO控制到TTS语音合成的工程细节告警的最终落点是人的感官因此硬件选型与驱动必须严丝合缝。Python无法直接驱动工业级声光器件必须通过中间层转换。我采用树莓派4B作为边缘网关其GPIO引脚通过光耦隔离后驱动继电器再控制24V直流蜂鸣器与LED灯组。语音部分则利用Linux系统的espeak-ng TTS引擎而非调用云端API——后者在网络中断时完全失效而工业现场网络稳定性远低于办公环境。3.1 声光器件选型与电气安全设计蜂鸣器选用12-24V宽压有源蜂鸣器型号PK-24A关键参数是“额定电压24VDC工作电流≤30mA”。有源蜂鸣器内置振荡电路只需提供直流电压即可发声避免Python生成PWM波形的复杂性。其1kHz基础频率符合GB/T 2893-2013对“警告音”的推荐频段800-1200Hz。LED灯组采用RGB三色LED模块共阴极通过三个GPIO分别控制R/G/B通道。红灯R通道用于一级告警黄灯G通道用于二级绿灯B通道用于三级。每个通道串联220Ω限流电阻确保GPIO输出电流10mA树莓派GPIO最大驱动能力16mA。电气隔离GPIO引脚不直接驱动负载而是通过PC817光耦隔离再驱动S8050三极管最后控制5V继电器线圈。此设计彻底切断树莓派与24V工业电源的地线耦合避免PLC地线干扰引入树莓派系统噪声。实测中未加光耦时PLC启停瞬间树莓派频繁重启加入光耦后连续运行30天无异常。注意树莓派GPIO电压为3.3V而光耦输入端需1.2V正向压降。若直接连接GPIO高电平3.3V经1kΩ电阻限流后光耦输入电流约2.1mA低于PC817典型值5mA。必须将限流电阻降至470Ω确保输入电流≥4.5mA否则光耦无法可靠导通。3.2 Python GPIO控制与多线程告警调度树莓派GPIO控制采用RPi.GPIO库但需注意其默认的BCM编号模式与物理引脚编号易混淆。我统一采用BOARD模式物理引脚号代码如下import RPi.GPIO as GPIO import threading import time class AlarmController: def __init__(self): GPIO.setmode(GPIO.BOARD) # 使用物理引脚编号 # 定义引脚11(Red), 13(Yellow), 15(Green), 16(Buzzer) self.RED_PIN 11 self.YELLOW_PIN 13 self.GREEN_PIN 15 self.BUZZER_PIN 16 self._setup_pins() def _setup_pins(self): GPIO.setup(self.RED_PIN, GPIO.OUT, initialGPIO.LOW) GPIO.setup(self.YELLOW_PIN, GPIO.OUT, initialGPIO.LOW) GPIO.setup(self.GREEN_PIN, GPIO.OUT, initialGPIO.LOW) GPIO.setup(self.BUZZER_PIN, GPIO.OUT, initialGPIO.LOW) def trigger_alarm(self, level, duration3): level: 1(红), 2(黄), 3(绿); duration: 告警持续秒数 # 先关闭所有灯和蜂鸣器 self._clear_all() if level 1: GPIO.output(self.RED_PIN, GPIO.HIGH) self._buzzer_on(1000, 0.5) # 1kHz, 占空比50% elif level 2: GPIO.output(self.YELLOW_PIN, GPIO.HIGH) self._buzzer_on(500, 0.3) # 500Hz, 占空比30% elif level 3: GPIO.output(self.GREEN_PIN, GPIO.HIGH) self._buzzer_on(300, 0.1) # 300Hz, 占空比10% # 启动定时关闭线程 threading.Thread(targetself._delay_off, args(level, duration), daemonTrue).start() def _buzzer_on(self, freq, duty_cycle): # 树莓派不支持硬件PWM用软件模拟 period 1.0 / freq on_time period * duty_cycle off_time period * (1 - duty_cycle) start_time time.time() while time.time() - start_time 0.5: # 持续0.5秒 GPIO.output(self.BUZZER_PIN, GPIO.HIGH) time.sleep(on_time) GPIO.output(self.BUZZER_PIN, GPIO.LOW) time.sleep(off_time) def _delay_off(self, level, duration): time.sleep(duration) if level 1: GPIO.output(self.RED_PIN, GPIO.LOW) elif level 2: GPIO.output(self.YELLOW_PIN, GPIO.LOW) elif level 3: GPIO.output(self.GREEN_PIN, GPIO.LOW) GPIO.output(self.BUZZER_PIN, GPIO.LOW)此代码的核心工程考量线程安全trigger_alarm方法启动的_delay_off线程为daemon避免主线程退出时告警未关闭。蜂鸣器驱动因树莓派无硬件PWM采用软件模拟。_buzzer_on中精确计算周期与占空比确保不同频率下声音特征稳定。实测1kHz蜂鸣器在50%占空比下响度最大而300Hz需降低占空比以避免嗡鸣感。状态隔离_clear_all()在每次触发前执行防止多级告警叠加导致灯光混乱。3.3 本地TTS语音合成espeak-ng的定制化调优espeak-ng是Linux下最轻量的开源TTS引擎安装命令sudo apt install espeak-ng。其优势在于纯本地运行、启动快100ms、资源占用低内存5MB。但默认语音机械感强需针对性调优# 生成语音文件的命令模板 espeak-ng -v zh -p 40 -s 150 -a 200 主轴温度超限请立即停机 --stdout /tmp/alarm.wav # 参数说明 # -v zh : 中文语音 # -p 40 : 音高pitch40范围0-9940为自然男声 # -s 150 : 语速speed150字/分钟标准值170降速提升清晰度 # -a 200 : 音量amplitude200范围0-200工业环境需高音量 # --stdout : 输出到标准输出便于Python管道捕获Python中调用并播放import subprocess import os def speak_text(text): # 生成临时WAV文件避免并发冲突 temp_file f/tmp/alarm_{int(time.time())}.wav cmd fespeak-ng -v zh -p 40 -s 150 -a 200 {text} --stdout {temp_file} subprocess.run(cmd, shellTrue) # 使用aplay播放树莓派默认音频输出为3.5mm接口 subprocess.run([aplay, temp_file]) # 清理临时文件 os.remove(temp_file) # 调用示例 speak_text(气源压力偏低检查空压机)调优关键点语音选择-v zh调用中文语音但espeak-ng的中文发音库mbrola-zh1需单独安装否则默认发音生硬。安装命令sudo apt install mbrola-zh1。语速与清晰度权衡工业现场背景噪音大语速150比170更易听清尤其对“超限”、“偏低”等关键词。音量最大化-a 200将输出音量推至极限配合24V功放驱动的喇叭实测1米距离声压级达85dB远超GB/T 2893-2013要求的70dB最低阈值。4. 调试全流程拆解从Wireshark抓包定位协议错误到PLC梯形图逻辑验证调试不是写完代码就结束而是贯穿开发、部署、运维的全生命周期活动。我将整个调试流程分为四个阶段每个阶段都有明确目标、工具和验收标准避免陷入“改一行代码测一小时”的低效循环。4.1 阶段一网络层连通性验证5分钟目标确认PLC与树莓派物理网络可达且Modbus TCP端口开放。工具pingtelnetnmap步骤ping 192.168.1.10确认IP可达。若不通检查网线、交换机、IP配置信捷PLC默认IP为192.168.1.10子网掩码255.255.255.0。telnet 192.168.1.10 502测试502端口是否监听。若连接失败说明PLC Modbus TCP服务未启用或防火墙拦截。nmap -p 502 192.168.1.10扫描端口状态确认为open而非filtered。注意信捷PLC的防火墙默认关闭但若PLC连接了多个网口如ETH0/ETH1需确认Modbus TCP服务绑定在正确网口。XG-Studio中“以太网设置”页面会显示当前生效的IP地址务必以此为准。4.2 阶段二协议层报文分析20分钟目标验证Modbus TCP请求/响应报文格式正确寄存器地址映射无误。工具Wireshark Modbus TCP过滤器步骤在树莓派上启动Wireshark捕获eth0接口流量。运行Python读取脚本触发一次寄存器读取。Wireshark中应用过滤器tcp.port 502找到Modbus TCP报文。展开报文检查事务标识符Transaction ID客户端与服务端匹配确认会话正常协议标识符Protocol ID应为0x0000非0则PLC不支持Modbus TCP长度字段Length计算值应等于后续字节数若不符则报文截断单元标识符Unit ID信捷PLC固定为1若设为其他值将拒绝响应功能码Function Code0x03表示读保持寄存器0x06表示写单个寄存器起始地址Starting Address检查是否为十进制40101对应D100而非0x9C45十六进制。实测案例某次调试中Wireshark显示PLC响应报文的功能码为0x830x030x80这是Modbus异常响应码表示“非法地址”。追踪发现Python代码中address101而信捷PLC D区起始地址设为0故D101对应Modbus地址40101但pymodbus内部将1014000140102导致读取40102失败。修正为address100后报文恢复正常。4.3 阶段三逻辑层联动验证30分钟目标确认PLC梯形图逻辑能正确改变寄存器值且Python能实时捕获变化。工具XG-Studio在线监控 Python日志打印步骤在XG-Studio中打开PLC程序定位到告警触发逻辑如急停按钮X0常闭触点串联D0置位线圈。启用“在线监控”观察D0寄存器值按下急停按钮D0应从0变为1。同时运行Python监控脚本打印D0读取值。理想状态是D0变1后Python日志在100ms内输出D01并触发红灯与语音。若Python未捕获变化检查XG-Studio是否处于“写入模式”而非“只读模式”否则D0值不会更新Python轮询频率是否足够100ms若设为1s则延迟过大PLC程序扫描周期是否过长信捷XC系列默认20ms若程序复杂可能达50ms。关键技巧在XG-Studio中右键D0寄存器选择“强制ON/OFF”可绕过梯形图逻辑直接修改寄存器值快速验证Python读取功能。这是排除PLC程序问题的最快方法。4.4 阶段四告警效果现场测试60分钟目标在真实产线环境中验证声光语音告警的可识别性与可靠性。工具声级计手机APP可替代、秒表、产线操作员反馈步骤将树莓派与声光器件安装在产线旁距离操作台1.5米。模拟一级告警D01测量红灯亮度使用手机照度APP应≥100luxGB/T 2893-2013要求蜂鸣器声压级手机分贝仪APP应≥80dB工业环境背景噪音约70dB语音清晰度邀请3名操作员背对设备听辨“主轴温度超限”是否准确要求识别率100%。连续触发告警10次记录告警响应延迟从D0变1到红灯亮起应≤150ms语音播放完成时间应≤2.5秒含生成与播放是否出现漏报10次中0次漏报为合格。实测数据在某轴承装配线一级告警平均响应延迟128ms语音识别率100%但二级告警黄灯500Hz蜂鸣在设备轰鸣背景下识别率仅70%。解决方案是将黄灯改为红黄双色交替闪烁并提高蜂鸣器占空比至50%最终识别率提升至95%。这印证了工业现场调试的核心原则理论参数必须让位于实际人因工程。5. 避坑指南那些让调试周期延长3倍的隐性陷阱与实战对策调试中最耗时的往往不是技术难题而是那些文档不提、论坛不说、只有踩过才懂的“隐性陷阱”。以下是我在12个产线项目中总结的5类高频坑点附带可立即执行的对策。5.1 陷阱一PLC寄存器地址“幽灵偏移”现象Python读取D100始终返回0但XG-Studio在线监控显示D1001。 根因信捷PLC的Modbus TCP服务存在一个隐藏特性——当PLC程序下载后若未执行“复位”操作Modbus地址映射表不会刷新。即使你在XG-Studio中修改了D区起始地址新设置也不会生效PLC仍按旧映射响应。 对策每次修改PLC通信设置后必须在XG-Studio中点击“PLC→复位→CPU复位”然后重新下载程序。复位后PLC会重新加载Modbus配置Wireshark中可见新的事务ID序列。5.2 陷阱二树莓派USB供电不足引发GPIO失灵现象声光告警偶尔失效重启树莓派后暂时恢复数小时后再次失效。 根因树莓派4B通过USB接口为WiFi网卡或USB转串口模块供电当总电流超过1.2A时USB电源管理芯片会降压导致GPIO输出电压跌至2.8V以下光耦无法导通。 对策为树莓派配备3A电源适配器并禁用USB供电echo max_usb_current1 | sudo tee -a /boot/config.txt然后重启。实测后GPIO电压稳定在3.3V±0.05V。5.3 陷阱三espeak-ng中文发音库缺失导致静音现象Python调用espeak-ng无任何声音输出aplay播放其他WAV文件正常。 根因espeak-ng -v zh命令依赖mbrola-zh1发音库但apt install espeak-ng默认不安装该库导致引擎回退到无发音的哑模式。 对策执行sudo apt install mbrola-zh1并验证发音库路径ls /usr/share/mbrola/zh1/应存在zh1目录。若仍无声运行espeak-ng -v zh --voices确认zh语音已注册。5.4 陷阱四Modbus TCP连接数超限引发“连接被拒绝”现象Python程序运行数小时后client.connect()返回FalseWireshark显示PLC发送RST包。 根因信捷PLC Modbus TCP服务器最大连接数为4而Python程序在异常断连后未主动关闭socket导致连接句柄泄漏。当泄漏连接数达4时新连接被拒绝。 对策在PLCReader类中添加连接清理机制def disconnect(self): if self.client and self.client.is_connected(): self.client.close() self.connected False并在主循环中每次轮询前检查连接状态断连时先disconnect()再connect()确保句柄及时释放。5.5 陷阱五PLC扫描周期与Python轮询频率共振现象告警响应忽快忽慢有时延迟200ms有时长达800ms。 根因PLC程序扫描周期如25ms与Python轮询间隔如100ms形成周期性相位差。当Python恰好在PLC扫描周期开始前读取获取的是旧值若在扫描结束后读取则获取新值。100ms与25ms的最小公倍数为100ms导致每4次轮询才命中一次新值。 对策将Python轮询间隔设为PLC扫描周期的奇数倍如75ms3×25ms或125ms5×25ms。实测75ms轮询下告警响应延迟稳定在100±10ms。这些陷阱的共同特点是它们都不在任何官方文档中明示却能在调试中吞噬数天时间。我的经验是每当调试卡顿超过2小时就拿出这张避坑清单逐条核对——90%的问题都能在15分钟内定位。真正的工业调试高手不是代码写得多好而是对这些“灰色地带”的熟悉程度有多深。6. 从单点告警到智能预警基于历史数据的故障预测扩展路径这套“PLC开口说话”系统其价值远不止于实时告警。当Python持续采集PLC寄存器数据并存储为时序数据库后它就进化为产线的“数字听诊器”。我以某注塑机为例展示如何基于现有架构进行低成本扩展。6.1 数据采集层升级从寄存器读取到时序存储在原有PLCReader类中增加数据持久化功能import sqlite3 from datetime import datetime class DataLogger: def __init__(self, db_path/var/log/plc_data.db): self.db_path db_path self._init_db() def _init_db(self): conn sqlite3.connect(self.db_path) conn.execute( CREATE TABLE IF NOT EXISTS sensor_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, register_address INTEGER, value INTEGER, plc_ip TEXT ) ) conn.close() def log_data(self, address, value, plc_ip192.168.1.10): conn sqlite3.connect(self.db_path) conn.execute( INSERT INTO sensor_data (register_address, value, plc_ip) VALUES (?, ?, ?), (address, value, plc_ip) ) conn.commit() conn.close() # 在轮询循环中调用 logger DataLogger() while True: d100_value reader.read_holding_registers(100)[0] logger.log_data(100, d100_value) # 记录D100值 time.sleep(0.1)SQLite数据库轻量可靠单日采集10万条数据仅占20MB空间且支持SQL查询。关键优势是无需额外部署InfluxDB或TimescaleDB树莓派资源完全够用。6.2 故障预测模型用滑动窗口统计识别异常趋势以注塑机“模具温度”寄存器D200为例正常值范围60-80℃但故障前常出现缓慢爬升。传统阈值告警80℃只能事后响应而滑动窗口统计可提前预警import numpy as np from collections import deque class AnomalyDetector: def __init__(self, window_size100): # 100个采样点约10秒数据 self.window deque(maxlenwindow_size) def update(self, value): self.window.append(value) if len(self.window) 50: # 窗口未满不计算 return False # 计算滑动平均与标准差 arr np.array(self.window) mean np.mean(arr) std np.std(arr) # 若当前值 均值3σ判定为异常上升 if value mean 3 * std: return True return False # 使用示例 detector AnomalyDetector() while True: temp reader.read_holding_registers(200)[0] if detector.update(temp): speak_text(模具温度异常上升检查冷却系统) time.sleep(0.1)此模型无需机器学习框架纯NumPy实现内存占用1MB。实测在注塑机案例中可在温度突破80℃前2-3分钟发出预警为操作员预留充足处置时间。6.3 可视化看板用Grafana构建免费监控大屏将SQLite数据导出为CSV再通过Grafana的SQLite插件接入即可构建实时看板面板1D200温度曲线图标注正常范围60-80℃与预警线75℃面板2告警统计饼图显示一级/二级/三级告警占比面板3设备在线状态绿色在线/红色离线指示灯。Grafana部署命令树莓派curl -fsSL https://packages.grafana.com/gpg.key | sudo apt-key add - echo deb https://packages.grafana.com/oss/deb stable main | sudo tee /etc/apt/sources.list.d/grafana.list sudo apt update sudo apt install grafana sudo systemctl enable grafana-server sudo systemctl start grafana