diff --git a/App/AT.cpp b/App/AT.cpp new file mode 100644 index 00000000..669e01d9 --- /dev/null +++ b/App/AT.cpp @@ -0,0 +1,397 @@ +#include "Kernel\Sys.h" +#include "Kernel\Task.h" +#include "Kernel\TTime.h" +#include "Kernel\WaitHandle.h" + +#include "Device\SerialPort.h" + +#include "AT.h" + +#include "Config.h" + +#include "App\FlushPort.h" + +#define NET_DEBUG DEBUG +//#define NET_DEBUG 0 +#if NET_DEBUG +#define net_printf debug_printf +#else +#define net_printf(format, ...) +#endif + +struct CmdState +{ + const String* Command = nullptr; + String* Result = nullptr; + cstring Key1 = nullptr; + cstring Key2 = nullptr; + cstring Key3 = nullptr; + + bool Capture = true; // 是否捕获所有 + + uint Parse(const Buffer& bs, WaitHandle& handle); + uint FindKey(const String& str); +}; + +void LoadStationTask(void* param); + +/* + 注意事项 +1、设置模式AT+CWMODE需要重启后生效AT+RST +2、AP模式下查询本机IP无效,可能会造成死机 +3、开启server需要多连接作为基础AT+CIPMUX=1 +4、单连接模式,多连接模式 收发数据有参数个数区别 +*/ + +/******************************** AT ********************************/ + +AT::AT() +{ + Port = nullptr; + DataKey = nullptr; + _Expect = nullptr; +} + +AT::~AT() +{ + delete Port; +} + +void AT::Init(COM idx, int baudrate) +{ + auto srp = new SerialPort(idx, baudrate); + srp->Tx.SetCapacity(0x200); + srp->Rx.SetCapacity(0x200); + srp->MaxSize = 512; + + Init(srp); +} + +void AT::Init(ITransport* port) +{ + Port = port; + if (Port) Port->Register(OnPortReceive, this); +} + +bool AT::Open() +{ + if (!Port->Open()) return false; + + /*if (!CheckReady()) + { + net_printf("AT::Open 打开失败!"); + + return false; + }*/ + + return true; +} + +void AT::Close() +{ + Port->Close(); +} + +// 发送指令,在超时时间内等待返回期望字符串,然后返回内容 +String AT::Send(const String& cmd, cstring expect, cstring expect2, uint msTimeout) +{ + TS("AT::Send"); + + String rs; + + auto& task = Task::Current(); + // 判断是否正在发送其它指令 + if (_Expect) + { +#if NET_DEBUG + auto h = (WaitHandle*)_Expect; + auto w = (CmdState*)h->State; + net_printf("AT::Send %d 正在发送 ", h->TaskID); + if (w->Command) + w->Command->Trim().Show(false); + else + net_printf("数据"); + net_printf(" ,%d 无法发送 ", task.ID); + cmd.Trim().Show(true); +#endif + + return rs; + } + + // 在接收事件中拦截 + CmdState we; + // 数据不显示Command,没有打开NET_DEBUG时也不显示 + //we.Command = &cmd; + we.Result = &rs; + we.Key1 = expect; + we.Key2 = expect2; + we.Key3 = "busy "; + + WaitHandle handle; + handle.State = &we; + + _Expect = &handle; + +#if NET_DEBUG + bool EnableLog = true; +#endif + + if (cmd) + { + Port->Write(cmd); + +#if NET_DEBUG + // 只有AT指令显示日志 + if (!cmd.StartsWith("AT") || (expect && expect[0] == '>')) EnableLog = false; + if (EnableLog) + { + we.Command = &cmd; + //net_printf("%d=> ", task.ID); + cmd.Trim().Show(true); + } +#endif + } + + handle.WaitOne(msTimeout); + if (_Expect == &handle) _Expect = nullptr; + + //if(rs.Length() > 4) rs.Trim(); + +#if NET_DEBUG + if (EnableLog && rs) + { + net_printf("%d<= ", task.ID); + // 太长时不要去尾,避免产生重新分配 + if (rs.Length() < 0x40) + rs.Trim().Show(true); + else + rs.Show(true); + } +#endif + + return rs; +} + +// 发送命令,自动检测并加上\r\n,等待响应OK +bool AT::SendCmd(const String& cmd, uint msTimeout) +{ + TS("AT::SendCmd"); + + static const cstring ok = "OK"; + static const cstring err = "ERROR"; + + String cmd2; + + // 只有AT指令需要检查结尾,其它指令不检查,避免产生拷贝 + auto p = &cmd; + if (cmd.StartsWith("AT") && !cmd.EndsWith("\r\n")) + { + cmd2 = cmd; + cmd2 += "\r\n"; + p = &cmd2; + } + + // 二级拦截。遇到错误也马上结束 + auto rt = Send(*p, ok, err, msTimeout); + + return rt.Contains(ok); +} + +bool AT::WaitForCmd(cstring expect, uint msTimeout) +{ + String rs; + + // 在接收事件中拦截 + CmdState we; + we.Result = &rs; + we.Key1 = expect; + we.Capture = false; + + WaitHandle handle; + handle.State = &we; + + _Expect = &handle; + + // 等待收到数据 + bool rt = handle.WaitOne(msTimeout); + + _Expect = nullptr; + + return rt; +} + +void ParseFail(cstring name, const Buffer& bs) +{ +#if NET_DEBUG + if (bs.Length() == 0) return; + + int p = 0; + if (p < bs.Length() && bs[p] == ' ') p++; + if (p < bs.Length() && bs[p] == '\r') p++; + if (p < bs.Length() && bs[p] == '\n') p++; + + // 无法识别的数据可能是空格前缀,需要特殊处理 + auto str = bs.Sub(p, -1).AsString(); + if (str) + { + net_printf("AT:%s 无法识别[%d]:", name, bs.Length()); + //if(bs.Length() == 2) net_printf("%02X %02X ", bs[0], bs[1]); + str.Show(true); + } +#endif +} + +uint AT::OnPortReceive(ITransport* sender, Buffer& bs, void* param, void* param2) +{ + auto esp = (AT*)param; + return esp->OnReceive(bs, param2); +} + +// 引发数据到达事件 +uint AT::OnReceive(Buffer& bs, void* param) +{ + if (bs.Length() == 0) return 0; + + //!!! 分析数据和命令返回,特别要注意粘包 + int s = 0; + int p = 0; + auto str = bs.AsString(); + while (p >= 0 && p < bs.Length()) + { + s = p; + p = str.IndexOf(DataKey, s); + + // +IPD之前之后的数据,留给命令分析 + int size = p >= 0 ? p - s : bs.Length() - s; + if (size > 0) + { + if (_Expect) + { + ParseReply(bs.Sub(s, size)); +#if NET_DEBUG + // 如果没有吃完,剩下部分报未识别 + //if(rs < size) ParseFail("ParseReply", bs.Sub(s + rs, size - rs)); + // 不要报未识别了,反正内部会全部吃掉 +#endif + } + else + { +#if NET_DEBUG + ParseFail("NoExpect", bs.Sub(s, size)); +#endif + } + } + + // +IPD开头的数据,作为收到数据 + if (p >= 0) + { + if (p + 5 >= bs.Length()) + { +#if NET_DEBUG + ParseFail("+IPD<=5", bs.Sub(p, -1)); +#endif + break; + } + else + { + auto bs2 = bs.Sub(p, -1); + Received(bs2); + int rs = bs2.Length(); + if (rs <= 0) + { +#if NET_DEBUG + ParseFail("ParseReceive", bs.Sub(p + rs, -1)); +#endif + break; + } + + // 游标移到下一组数据 + p += rs; + } + } + } + + return 0; +} + +// 分析关键字。返回被用掉的字节数 +uint AT::ParseReply(const Buffer& bs) +{ + if (!_Expect) return 0; + + // 拦截给同步方法 + auto handle = (WaitHandle*)_Expect; + auto we = (CmdState*)handle->State; + bool rs = we->Parse(bs, *handle); + + // 如果内部已经适配,则清空 + if (!we->Result) _Expect = nullptr; + + return rs; +} + +uint CmdState::Parse(const Buffer& bs, WaitHandle& handle) +{ + if (bs.Length() == 0 || !Result) return 0; + + TS("WaitExpect::Parse"); + + // 适配任意关键字后,也就是收到了成功或失败,通知业务层已结束 + auto s_ = bs.AsString(); + auto& s = (const String&)s_; + int p = FindKey(s); + auto& rs = *Result; + + // 捕获所有 + if (Capture) + { + if (p > 0) + rs += bs.Sub(0, p).AsString(); + else + rs += s; + } + else if (p > 0) + rs = bs.Sub(0, p).AsString(); + + // 匹配关键字,任务完成 + if (p > 0) + { + Result = nullptr; + + // 设置事件,通知等待任务退出循环 + handle.Set(); + } + + // 如果后面是换行,则跳过 + if (p < s.Length() && s[p] == ' ') p++; + if (p < s.Length() && s[p] == '\r') p++; + if (p < s.Length() && s[p] == '\n') p++; + + return p; +} + +uint CmdState::FindKey(const String& str) +{ + // 适配第一关键字 + int p = Key1 ? str.IndexOf(Key1) : -1; + if (p >= 0) + { + //net_printf("适配第一关键字 %s \r\n", Key1); + return p + String(Key1).Length(); + } + // 适配第二关键字 + p = Key2 ? str.IndexOf(Key2) : -1; + if (p >= 0) + { + net_printf("适配第二关键字 %s \r\n", Key2); + return p + String(Key2).Length(); + } + // 适配busy + p = str.IndexOf(Key3); + if (p >= 0) + { + net_printf("适配第三关键字 %s \r\n", Key3); + return p + String(Key3).Length(); + } + return 0; +} diff --git a/App/AT.h b/App/AT.h new file mode 100644 index 00000000..0385e0a2 --- /dev/null +++ b/App/AT.h @@ -0,0 +1,43 @@ +#ifndef __AT_H__ +#define __AT_H__ + +// GPRS的AT指令集 GSM 07.07 +class AT +{ +public: + ITransport* Port; // 传输口 + + cstring DataKey; // 数据关键字 + + Delegate Received; + + AT(); + ~AT(); + + void Init(COM idx, int baudrate = 115200); + void Init(ITransport* port); + + // 打开与关闭 + bool Open(); + void Close(); + + /******************************** 发送指令 ********************************/ + // 发送指令,在超时时间内等待返回期望字符串,然后返回内容 + String Send(const String& cmd, cstring expect, cstring expect2 = nullptr, uint msTimeout = 1000); + // 发送命令,自动检测并加上\r\n,等待响应OK + bool SendCmd(const String& cmd, uint msTimeout = 1000); + // 等待命令返回 + bool WaitForCmd(cstring expect, uint msTimeout); + +private: + void* _Expect; // 等待内容 + + // 分析关键字。返回被用掉的字节数 + uint ParseReply(const Buffer& bs); + + // 引发数据到达事件 + uint OnReceive(Buffer& bs, void* param); + static uint OnPortReceive(ITransport* sender, Buffer& bs, void* param, void* param2); +}; + +#endif diff --git a/Board/AP0803.cpp b/Board/AP0803.cpp index 94823563..1113d4b1 100644 --- a/Board/AP0803.cpp +++ b/Board/AP0803.cpp @@ -5,9 +5,7 @@ #include "Device\WatchDog.h" #include "Config.h" -#include "Drivers\NRF24L01.h" -#include "Drivers\W5500.h" -#include "Drivers\Esp8266\Esp8266.h" +#include "Drivers\GSM07.h" #include "TokenNet\TokenController.h" #include "TokenNet\TokenConfig.h" @@ -124,52 +122,22 @@ void AP0803::InitButtons(const Delegate2& press) } } -NetworkInterface* AP0803::Create5500() +NetworkInterface* AP0803::CreateGPRS() { - debug_printf("\r\nW5500::Create \r\n"); + debug_printf("\r\nCreateGPRS::Create \r\n"); - auto net = new W5500(Spi2, PE1, PD13); + auto net = new GSM07(); + net->Init(COM4); + net->Set(P0, P0); if(!net->Open()) { delete net; return nullptr; } - net->EnableDNS(); - net->EnableDHCP(); - return net; } -NetworkInterface* AP0803::Create8266(bool apOnly) -{ - debug_printf("\r\nEsp8266::Create \r\n"); - - auto esp = new Esp8266(COM4, PE2, PD3); - - // 初次需要指定模式 否则为 Wire - bool join = esp->SSID && *esp->SSID; - if (!join) - { - *esp->SSID = "WSWL"; - *esp->Pass = "12345678"; - - esp->Mode = NetworkType::STA_AP; - esp->WorkMode = NetworkType::STA_AP; - } - - if(!esp->Open()) - { - delete esp; - return nullptr; - } - - Client->Register("SetWiFi", &Esp8266::SetWiFi, esp); - Client->Register("GetWiFi", &Esp8266::GetWiFi, esp); - - return esp; -} - /******************************** Token ********************************/ void AP0803::InitClient() @@ -209,8 +177,7 @@ static void OnInitNet(void* param) { auto& bsp = *(AP0803*)param; - bsp.Create5500(); - bsp.Create8266(false); + bsp.CreateGPRS(); Client->Open(); } diff --git a/Board/AP0803.h b/Board/AP0803.h index 8708b53e..9b50beb9 100644 --- a/Board/AP0803.h +++ b/Board/AP0803.h @@ -40,12 +40,8 @@ public: void InitLeds(); void InitButtons(const Delegate2& press); - // 打开以太网W5500 - NetworkInterface* Create5500(); - // 打开Esp8266,作为主控或者纯AP - NetworkInterface* Create8266(bool apOnly); - - // ITransport* Create2401(); + // 打开GPRS + NetworkInterface* CreateGPRS(); void InitClient(); void InitNet(); diff --git a/Drivers/Esp8266/WaitExpect.cpp b/Drivers/Esp8266/WaitExpect.cpp index f0845871..3a8f5edc 100644 --- a/Drivers/Esp8266/WaitExpect.cpp +++ b/Drivers/Esp8266/WaitExpect.cpp @@ -18,21 +18,6 @@ bool WaitExpect::Wait(int msTimeout) Sys.Sleep(40); if(!Result) return true; - /*// 等待收到数据 - TimeWheel tw(msTimeout - 40); - // 默认检查间隔200ms,如果超时时间大于1000ms,则以四分之一为检查间隔 - // ESP8266串口任务平均时间为150ms左右,为了避免接收指令任务里面发送指令时等不到OK,需要加大检查间隔 - tw.Sleep = 200; - if(msTimeout > 1000) tw.Sleep = msTimeout >> 2; - if(tw.Sleep > 1000) tw.Sleep = 1000; - - while(Result) - { - if(tw.Expired()) return false; - } - - return true;*/ - return Handle.WaitOne(msTimeout); } diff --git a/Drivers/GSM07.cpp b/Drivers/GSM07.cpp new file mode 100644 index 00000000..c484d2d3 --- /dev/null +++ b/Drivers/GSM07.cpp @@ -0,0 +1,324 @@ +#include "Kernel\Sys.h" +#include "Kernel\Task.h" +#include "Kernel\TTime.h" +#include "Kernel\WaitHandle.h" + +#include "Device\SerialPort.h" + +#include "GSM07.h" + +#include "Config.h" + +#include "App\FlushPort.h" + +#define NET_DEBUG DEBUG +//#define NET_DEBUG 0 +#if NET_DEBUG +#define net_printf debug_printf +#else +#define net_printf(format, ...) +#endif + +struct CmdState +{ + String* Command = nullptr; + String* Result = nullptr; + cstring Key1 = nullptr; + cstring Key2 = nullptr; + + bool Capture = true; // 是否捕获所有 +}; + +/* + 注意事项 +1、设置模式AT+CWMODE需要重启后生效AT+RST +2、AP模式下查询本机IP无效,可能会造成死机 +3、开启server需要多连接作为基础AT+CIPMUX=1 +4、单连接模式,多连接模式 收发数据有参数个数区别 +*/ + +/******************************** GSM07 ********************************/ + +GSM07::GSM07() +{ + Name = "GSM07"; + Speed = 10; + + APN = "CMNET"; + + Led = nullptr; + + Buffer(_sockets, 5 * 4).Clear(); + + Mode = NetworkType::STA_AP; + + InitConfig(); + LoadConfig(); +} + +GSM07::~GSM07() +{ + RemoveLed(); +} + +void GSM07::Init(COM idx, int baudrate) +{ + At.Init(idx, baudrate); +} + +void GSM07::Init(ITransport* port) +{ + At.Init(port); +} + +void GSM07::Set(Pin power, Pin rst) +{ + if (power != P0) _Power.Init(power, false); + if (rst != P0) _Reset.Init(rst, true); + +} + +void GSM07::SetLed(Pin led) +{ + if (led != P0) + { + auto port = new OutputPort(led); + SetLed(*port); + } +} + +void GSM07::SetLed(OutputPort& led) +{ + auto fp = new FlushPort(); + fp->Port = &led; + fp->Start(); + Led = fp; +} + +void GSM07::RemoveLed() +{ + if (Led)delete (FlushPort*)Led; + Led = nullptr; +} + +bool GSM07::OnOpen() +{ + if (!At.Open()) return false; + + if (!CheckReady()) + { + net_printf("GSM07::Open 打开失败!"); + + return false; + } + + At.Received.Bind(&GSM07::OnReceive, this); + + // 开回显 + Echo(true); + +#if NET_DEBUG + // 获取版本 + GetVersion(); + //auto ver = GetVersion(); + //net_printf("版本:"); + //ver.Show(true); +#endif + + Config(); + + return true; +} + +bool GSM07::CheckReady() +{ + // 先关一会电,然后再上电,让它来一个完整的冷启动 + if (!_Power.Empty()) + { + _Power.Open(); // 使用前必须Open; + _Power.Down(20); + } + if (!_Reset.Empty()) _Reset.Open(); // 使用前必须Open; + + // 每两次启动会有一次打开失败,交替 + if (!_Reset.Empty()) + Reset(false); // 硬重启 + else + Reset(true); // 软件重启命令 + + // 等待模块启动进入就绪状态 + if (!Test()) + { + net_printf("GSM07::Open 打开失败!"); + + return false; + } + + return true; +} + +void GSM07::OnClose() +{ + // 先断开已有连接 + At.SendCmd("AT+CIPSHUT\r"); + + At.Close(); + + _Power.Close(); + _Reset.Close(); +} + +bool GSM07::OnLink(uint retry) +{ + //if(Linked) return true; + + debug_printf("GSM07::OnLink\r\n"); + + /*bool join = SSID && *SSID; + // 等待WiFi自动连接 + if (!WaitForCmd("WIFI CONNECTED", 3000)) + { + auto mode = WorkMode; + // 默认Both + if (mode == NetworkType::Wire) mode = NetworkType::STA_AP; + if (!join || mode == NetworkType::STA_AP) OpenAP(); + if (join) + { + if (!JoinAP(*SSID, *Pass)) return false; + + ShowConfig(); + SaveConfig(); + } + }*/ + + return true; +} + +// 配置网络参数 +void GSM07::Config() +{ + // ATE0 关闭回显 + // ATE1 开启回显 + At.SendCmd("ATE0\r"); + At.SendCmd("AT+CIPSHUT\r"); + At.SendCmd("AT+CGCLASS=\"B\"\r"); + SetAPN(APN, false); + At.SendCmd("AT+CGATT=1\r"); + // 先断开已有连接 + //At.SendCmd("AT+CIPSHUT\r"); + //设置APN + SetAPN(APN, true); + At.SendCmd("AT+CLPORT=\"UDP\",\"3399\"\r"); + // IP设置方式 + //At.SendCmd("AT+CIPSTART=\"UDP\",\"183.63.213.113\",\"3388\"\r"); + // 域名设置方式 + At.SendCmd("AT+CIPMUX=0\r"); + At.SendCmd("AT+CIPRXGET=1\r"); + At.SendCmd("AT+CIPQRCLOSE=1\r"); + At.SendCmd("AT+CIPMODE=0\r"); +} + +Socket* GSM07::CreateSocket(NetType type) +{ + /*auto es = (EspSocket**)_sockets; + + int i = 0; + for (i = 0; i < 5; i++) + { + if (es[i] == nullptr) break; + } + if (i >= 5) + { + net_printf("没有空余的Socket可用了 !\r\n"); + + return nullptr; + }*/ + + switch (type) + { + /*case NetType::Tcp: + return es[i] = new EspTcp(*this, i); + + case NetType::Udp: + return es[i] = new EspUdp(*this, i);*/ + + default: + return nullptr; + } +} + +// 数据到达 +void GSM07::OnReceive(Buffer& bs) +{ + Received(bs); +} + +/******************************** 基础AT指令 ********************************/ + +// 基础AT指令 +bool GSM07::Test() +{ + // 避免在循环内部频繁构造和析构 + String cmd = "AT"; + for (int i = 0; i < 10; i++) + { + if (At.SendCmd(cmd, 500)) return true; + + Reset(false); + } + + return false; +} + +bool GSM07::Reset(bool soft) +{ + if (soft) return At.SendCmd("AT+RST"); + + _Reset.Up(100); + return true; +} + +/* +AT 版本信息 +基于的SDK版本信息 +编译生成时间 +*/ +String GSM07::GetVersion() +{ + return At.Send("AT+GMR\r\n", "OK"); +} + +bool GSM07::Sleep(uint ms) +{ + String cmd = "AT+GSLP="; + cmd += ms; + return At.SendCmd(cmd); +} + +bool GSM07::Echo(bool open) +{ + String cmd = "ATE"; + cmd = cmd + (open ? '1' : '0'); + return At.SendCmd(cmd); +} + +// 恢复出厂设置,将擦写所有保存到Flash的参数,恢复为默认参数。会导致模块重启 +bool GSM07::Restore() +{ + return At.SendCmd("AT+RESTORE"); +} + +// 00:CMNET 10:CMHK 01:CHKT 11:HKCSL +void GSM07::SetAPN(cstring apn, bool issgp) +{ + String str; + if(issgp) + str = "AT+CIPCSGP=1"; + else + str = "AT+CGDCONT=1,\"IP\""; + str = str + ",\"" + apn + "\"\r"; + + At.SendCmd(str); +} + +/******************************** TCP/IP ********************************/ diff --git a/Drivers/GSM07.h b/Drivers/GSM07.h new file mode 100644 index 00000000..4b88c706 --- /dev/null +++ b/Drivers/GSM07.h @@ -0,0 +1,84 @@ +#ifndef __GSM07_H__ +#define __GSM07_H__ + +#include "Device\Port.h" + +#include "App\AT.h" + +#include "Net\Socket.h" +#include "Net\NetworkInterface.h" +#include "Net\ITransport.h" + +#include "Message\DataStore.h" +#include "Message\Pair.h" + +// GPRS的AT指令集 GSM 07.07 +class GSM07 : public NetworkInterface +{ +public: + AT At; // AT操作对象 + cstring APN; + + IDataPort* Led; // 指示灯 + + OutputPort _Power; + OutputPort _Reset; + OutputPort _LowPower; + + Delegate Received; + + GSM07(); + virtual ~GSM07(); + + void Init(ITransport* port); + void Init(COM idx, int baudrate = 115200); + void Set(Pin power = P0, Pin rst = P0); + + virtual void Config(); + void SetLed(Pin led); + void SetLed(OutputPort& led); + void RemoveLed(); + + virtual Socket* CreateSocket(NetType type); + +/******************************** 基础AT指令 ********************************/ + bool Test(); + bool Reset(bool soft); + String GetVersion(); + bool Sleep(uint ms); + bool Echo(bool open); + // 恢复出厂设置,将擦写所有保存到Flash的参数,恢复为默认参数。会导致模块重启 + bool Restore(); + void SetAPN(cstring apn, bool issgp); + +/******************************** 功能指令 ********************************/ + IPAddress GetIP(bool sta); + +/******************************** TCP/IP ********************************/ + +/******************************** 发送指令 ********************************/ + +private: + IPEndPoint _Remote; // 当前数据包远程地址 + + // 打开与关闭 + virtual bool OnOpen(); + virtual void OnClose(); + // 检测连接 + virtual bool OnLink(uint retry); + + bool CheckReady(); + + // 多个硬件socket + int* _sockets[5]; + + // 分析+IPD接收数据。返回被用掉的字节数 + uint ParseReceive(const Buffer& bs); + // 分析关键字。返回被用掉的字节数 + uint ParseReply(const Buffer& bs); + + // 数据到达 + void OnReceive(Buffer& bs); +}; + +#endif diff --git a/vs/SmartOS.vcxproj b/vs/SmartOS.vcxproj index e4edf2d3..fd38fc64 100644 --- a/vs/SmartOS.vcxproj +++ b/vs/SmartOS.vcxproj @@ -13,6 +13,7 @@ + @@ -31,6 +32,7 @@ + @@ -84,6 +86,7 @@ + diff --git a/vs/SmartOS.vcxproj.filters b/vs/SmartOS.vcxproj.filters index 9a2fff15..640a60bc 100644 --- a/vs/SmartOS.vcxproj.filters +++ b/vs/SmartOS.vcxproj.filters @@ -560,5 +560,14 @@ Drivers\Esp8266 + + Board + + + Drivers + + + App + \ No newline at end of file