真正的智能盒子
组件和用品
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 2 | ||||
| × | 1 |
必要的工具和机器
| ||||
|
应用和在线服务
| ||||
|
关于这个项目
真正的智能盒子平台将真正有用的盒子 (tm) 变成一个智能的、互联网连接的存储盒,用于库存监控。基于 Sigfox Arduino MKR FOX 1200,它可以感知存储在盒子中的物品的重量以及温度和湿度,并使用低功率 Sigfox 无线电来传递这些信息。
用例 - 3D 打印机耗材存储:
如果您拥有一台 3D 打印机,您很可能会关心您的耗材是如何存储的,这不仅适用于打印机耗材,而且许多其他东西都需要存储在可接受的温度和湿度范围内(例如,油漆工的填缝剂如果暴露在外可能会变得无法使用)到冰点以下)。
作为负责维护本地创客空间 3D 打印机的人员之一,我需要确保我们有足够的耗材库存并保持干燥。
使用真正的智能盒子,我能够监测灯丝的重量,因此知道我们是否变低,同时监测盒子中的湿度水平,以确定硅胶是否需要更换。
图>
用例 - 消耗品库存控制:
清洁承包商可能希望在客户现场保留空气清新剂、洗手液或其他消耗品的库存,客户可能不会允许承包商使用 WiFi 访问或在不存在的情况下为此类设备供电,但是承包商公司需要知道何时发送新库存,增加了清洁工时间的开销和没人喜欢的额外文书工作。
Real Smart Box 平台只需放入一个存储盒中,因为它使用 Sigfox,它不需要连接到客户端网络并且功耗低,因此可以使用一组电池运行。由于盒子里的东西很少变化,Arduino 可以在大部分时间保持低功耗状态,有助于延长电池寿命。
平台可以获知储存在盒子中的物品类型(即空气清新剂)的重量,从而计算出盒子中有多少。然后可以将其发送给清洁承包商公司,以在他们需要将更多的炉灶运送到客户现场时提醒他们。
图>
制作平台:
平台结构简单,主体由两片激光切割亚克力(我用的是3mm,厚一点的适合打印机灯丝等重物),中间有一对称重传感器。
我手动对称重传感器的螺丝孔进行了埋头处理,以获得更好的效果,我还没有找到可以进行埋头处理的激光切割机!
亚克力可以切割成任何尺寸以匹配您喜欢的盒子,但是请注意称重传感器的位置和电线的长度,因为它相当短。上亚克力板比下亚克力板略小,以确保它不会挂在盒子侧面。
下层有一个切口,允许将电子设备安装到亚克力上而无需垫片,并使焊接的设备支腿穿过。我在切口的角落部分使用了 M3 龙头,将 PCB 直接固定在上面。 3D 打印的支脚也安装在角落处,以确保称重传感器上未与亚克力齐平的任何螺钉不会影响平衡。
图>使用两个 5Kg 称重传感器检测重量。一组 4 通常用于浴室秤,这可能会更好,但是我找不到将它们固定到亚克力以及提供电子设备所需间距的好方法。
称重传感器的顶部和底部需要一些填充物以允许稍微弯曲,并且称重传感器的实际应变计传感器(图片中的白色部分)比安装块更胖。这是在称重传感器下方通过两个“真正智能盒”3D 打印端板实现的,端板有一个小块可以将称重传感器升高,称重传感器顶部是一些激光切割的亚克力垫块。
称重传感器连接到 HX711 称重传感器放大器。它有两个通道(A 和 B)可供选择,非常适合此用途。
每个称重传感器均由惠斯通电桥配置中的应变计构成,这会产生一对不平衡的分压器,当称重传感器置于负载下时,应变计的电阻会发生变化,因此会产生两个分压器之间的差异,这是由为我们进行模数转换的HX711放大和测量的。
我在这个项目中使用了两个 5kg 称重传感器,您可以获得不同的额定值(例如 1kg 和 10kg),它们的工作原理完全相同,但灵敏度不同。
放置称重传感器时,确保传感器末端的箭头指向下方(在负载方向上)。电池的一端(通常是固定端)有 M5 螺纹孔,另一端(放置负载的一侧)有 M4 螺纹孔。
红/黑线是电源,它馈送分压器的顶部和底部,并在两个称重传感器之间共享。绿色和白色是分压器中间的感应线,它们连接到HX711上的通道A和B。
HX711 支持 3 个增益因子,但这些也用于通道选择。 A 通道提供 128 和 64 的增益,而选择 32 的增益则选择 B 通道。这意味着我们的第二个频道不会像主频道那样敏感,这对这个应用程序来说很好。
HX711可以连接到Arduino上的任何数字引脚,我用过D0(数据)和D1(时钟),那么放大器只需要连接到Arduino的3v3电源即可。
图>您可以在 SparkFuns 优秀的称重传感器教程中阅读有关称重传感器和 HX711 的更多信息。
最后一个 BME280 连接到 I2C 总线并用于感应盒子内的温度和湿度,这也可以用来感应压力,但这可能没什么兴趣,我们只有 12 字节的 sigfox 数据可以玩,所以它是未报告。
电子设备安装在 ThingySticks Arduino 原型板上,我添加了一个电池座(热熔胶粘在下部亚克力板上)并连接了天线,这是一个很好的平面设计,非常适合平台。
称重传感器校准:
在我们可以使用平台之前,称重传感器需要校准。每个称重传感器都是独一无二的,它是通过将应变计连接到一块金属上并在其中钻出的孔制成的,以提供足够的弯曲度,而不会折断,因此我们需要校准每个称重传感器对重量的响应.
校准后,我们将 y=mx+c 方程应用于测量的 ADC 值 (x) 以获得实际重量 (y)。所以我们需要为我们的称重传感器找到 c(偏移量)和 m(斜率)。
我取下主平台顶部,依次将一个小的亚克力方块连接到每个称重传感器,并监控测量值(固件中的路由可以通过向串行端口发送“c”来启动。
最初测量空平台的读数,这给出了偏移 (c) 值,然后在电池上放置已知重量的负载,读数的差异给出了斜率。
斜率 =(测量值 - 偏移量)/重量(g)。
我同时使用了一个小空气清新剂罐(约 230 克)和一卷打印机耗材(约 1.5 公斤)来检查数值,两者的斜率大致相同,令人放心。
图>自然地,使用小亚克力垫测量的偏移量与使用完整顶板所经历的偏移量不同,同样,当使用两个称重传感器时,它们的斜率差异也很小,因此需要进行二次校准。现在使用一点零偏移(皮重),这是在固件中设置的,但也可以使用 USB 串行连接或部署后通过 Sigfox 下行链路消息进行设置。
Sigfox 连接:
随着真正的智能盒接线,我最初使用 USB 串行端口来监视输出以帮助调试和调整系统。通过这种方式,您可以看到各个称重传感器、变化和噪音。然而,这不适用于已部署的盒子,因为它需要完全无线。
使用 Sigfox,我们每天最多可以向我们的在线服务发送 12 字节的数据 140 次,这对于真正的智能盒子来说已经足够了。下面在Arduino中使用的数据结构描述了我们如何使用12个字节。
typedef struct __attribute__ ((packed)) sigfox_message { uint8_t status; // 状态标志 int8_t 湿度; // 湿度::int:8 - 一些传感器 (HTU21D) 读取 -ve 湿度) int8_t 温度; // 温度::int:8(无小数位)。 int16_t 零权重; // zeroWeight::int:16:little-endian int16_t weight; // weight::int:16:little-endian int16_t itemCount; // itemCount::int:16:little-endian(100x 实际项目计数以允许 2.01(因为重量不会完全匹配) int8_tdriftCorrection; // 应用于秤的零重量变化的漂移校正。 int8_tfiller; // 这里没什么可看的,继续.... int8_t lastStatus; // 最后的 sigfox 状态 } SigfoxMessage;
第一个字节(状态)被分成位标志以指示问题:
// status::uint:8 -> 拆分为8位 // B7 - 第一次运行// B6 - HX711 故障 // B5 - BME280 故障// B4 - 温度报警// B3 - 湿度报警// B2 - 重量警报// B1 - 低库存// B0 - 备用
这个结构压缩到 12 个字节,但是我们需要在 Sigfox 端解压它以推送到 Tinamous。我们为此使用自定义有效负载配置,最好在定义数据结构时解决这个问题。我们的是:
firstRun::bool:7 hx711Fault::bool:6 bmeFault::bool:5 temperatureAlarm::bool:4 湿度Alarm::bool:3 weightAlarm::bool:2 lowStock::bool:1 b0::bool:0 status::int:8 湿度::int:8 温度::int:8 zeroWeight::int:16:little-endian weight::int:16:little-endian itemCount::int:16:little -endian
自定义负载在解析时将我们的 12 个字节拆分。
请注意,我们需要指定任何大于 1 字节的小端性质,因为 Sigfox 默认为大端,而 Arduino 使用小端(即最低有效字节在多字节字中排在第一位)。
另请注意,在第一个字节中拆分布尔标志不会像所有其他读取一样推进字节标记,因此也会读取包含所有标志的状态字节以跳过第一个字节。
标志中包括温度、湿度和重量范围警报标志,我们可以使用在线服务(即 Tinamous)来监控超出范围的温度、湿度和重量,但是这些可能是短暂的(几个小时)和我们的盒子可能不经常发送(一天一次或两次),因此很容易错过由此产生的可能造成破坏的环境条件,因此它们会在设备上进行标记并发送(并在成功发送后重置)。
项目计数实际上设置为实际项目计数的 100 倍。我想在不强制四舍五入的情况下允许 2.2 项(由于重量错误或其他项目)之类的值,同样,如果我们不小心,2.95 可能会四舍五入为 2,这会更暗示 3 项在盒子和一个小错误。我也不想使用需要更多空间的浮点数,所以我使用了 16 位字并应用了一个因子以允许轻松转换(它也被签名以允许零错误,这可能导致库存水平-1 或 -2 等)。
只需很少的工作即可启用 Sigfox 通信。根据 Sigfox 库的 Arduino 示例,在 Arduino 中添加了 Sigfox 库并调用了适当的函数来推送数据,但是我们需要向 Sigfox 注册我们的设备。
向Really Smart Box 的串行端口发送“s”会打印Sigfox ID 和PAC 代码,这些用于在Sigfox 后端激活设备。然后我们转到 Sigfox 后端激活服务并按照向导进行操作,首先选择我们的设备,然后是国家/地区/提供商,然后是一些详细信息。
图> 图> 图>最后我们的设备被激活并列出:
Sigfox 将设备分配到设备类型分组,这是明智的,因为您通常可能有许多(数百、数千等)相同的设备类型,您希望作为一个组进行操作。定义了设备类型后,我们可以配置一个自定义回调来将我们收到的数据推送到我们的在线服务。为此,我正在使用 Tinamous(提示:查看我的个人资料名称 - 我的选择可能有偏差)。
使用真正智能的盒子:
连接好、用螺栓固定在一起、固件闪烁并安装好电池后,平台只需放入真正有用的盒子 (tm) 中即可使用。
应尽可能晚地通电,因为一旦设备通电,它将在 2 分钟后发送第一条 Sigfox 消息并请求下行数据。该数据可以包括将平台重量归零的“归零”命令。如果失败,要么需要 USB 串行连接,要么等待下一个下行链路请求 - 这些请求每 12 小时发出一次。
一旦启动并运行,平台将每 15 分钟发布一次 Sigfox 消息,以发送重量、物品数量、温度、湿度和警报状态。每分钟测量一次温度、湿度和重量,以确保它们不会超出范围,如果它们已发出,则会标记为下一次传输。
使用 Tinamous 进行监控:
Tinamous 通过向我们的帐户添加“Sigfox Bot”来支持 Sigfox 自定义回调,有关如何执行此操作的说明,请参阅我的“使用 Sigfox”Hackster.io 教程。
将 Sigfox Bot 添加到您的 Tinamous 帐户时,如果您包含 API 设置,Sigfox Bot 将查找您的设备并将它们添加到您的 Tinamous 帐户,但是您不需要这样做,因为设备会在数据时自动添加已发布。
添加机器人后,我们会看到一个回调配置屏幕,以帮助设置 Sigfox 回调。
然后,您可以在 Sigfox 创建自定义回调,请注意,Really Smart Box 使用 DATA -> BIDIR 回调处理正常的上行链路回调和 BIDIR(上行和下行链路)回调。
这是之前的自定义负载派上用场的地方,将其从源代码粘贴到自定义负载并更新字段部分以反映这一点。
Lat 和 Lng 在这个回调中指定,给出了一个大概的位置,但是 Arduino 上的 Sigfox 支持改进的位置设置,但这需要第二个回调。如果您使用地理位置工具,请不要在此消息中指定纬度/经度,因为真正的智能盒子将在位置之间移动。
配置完成后,还需要为下行链路启用,即使设置了 BIDIR,默认情况下也是禁用的。
请注意,下行选项下方的屏幕截图已“选中”,这必须手动完成,如果下行数据的设备类型未设置为“回调”(设备类型 -> 编辑 -> 下行数据),则可能不可用.
图>对于下行链路回调,我们还想指定一个 SERVICE -> ACKNOWLEDGE 回调来知道我们的设备已收到下行链路数据。通过单击 Tinamous 中的 Sigfox Bot 显示其他回调配置,请按照 ACKNOWLEDGE 和 GEOLOC 回调的说明进行操作。
请注意,您需要从第一个上行链路/bidir 回调中复制授权标头,因为这是在 Tinamous 中单向加密的密码,不再可用于显示。
图>有了我们的回调,我们设备发布的数据现在应该被发送到 Tinamous。我们还可以在 Sigfox 添加电子邮件回调,这可能有助于确认数据正在通过(但也可能很快变得非常嘈杂)。
配置 Tinamous 设备:
一旦在 Tinamous 上看到 Sigfox 设备(通过 api 查找或回调),它将显示在设备页面上,我们可以从这里编辑属性。字段会自动添加,因为它们来自 Sigfox 回调,因此最好等到设备发布数据。
我将“Not Reporting After”时间设置为 1 小时(目前每 15 分钟发布一次),这样我就可以判断设备是否损坏并可以选择收到有关此的通知。
我不想在图表/详细信息页面上看到设备发送的所有字段(如果包含所有标志,它们会很多),因此 Tinamous 仅配置为显示重量和项目计数。人类友好的标签和单位也被应用在这里。
项目计数字段是实际项目计数的 100 倍,因此对该字段应用校准以将其减少 100 倍。
某些下行数据设置将导致Really Smart Box 归零并在下一次请求下行消息时应用温度和湿度范围限制(开机后2 分钟,然后每12 小时一次)。
查看真正的智能盒子信息:
现在设备字段已配置,我们可以通过设备详细信息页面监控它们(注意我此时不会将平台归零,因此它认为存在 1/2 一个单元 - 我还用 5mm 替换了亚克力顶部较重但能更好地处理打印机灯丝的版本)。
我们还可以从 Sigfox 部分看到 Sigfox 回调交互。请注意,正在发送和确认下行链路数据,但 Arduino 报告错误。后面会详细介绍。
在“位置”选项卡上,我们还可以看到我们的“真正智能盒子”在哪里,如果您忘记了它是在哪个客户那里,或者它在一辆面包车里,这可能会很有用。
很自然地,我们想要一个很好的真正智能盒子的仪表板视图,下面的显示盒子内容物的重量,其中的估计单位,以及未报告的设备数量,以便我们可以判断一个设备是否损坏。
使用 Tinamous 接收通知:
接下来,我将 Tinamous 设置为在项目数量较少时发送电子邮件和短信。我通过为项目计数字段指定 3 - 300 的工作范围来做到这一点。如果该值超出此范围,则甚至会提高超出范围的测量值。
向 Tinamous 添加通知,我们可以在发生这种情况时收到通知。
我们可以只指定我们感兴趣的字段,但将其留空会通知我们任何超出范围的字段。
同样对于设备,对于所有设备都留空(即我们目前唯一的设备!)
将重复通知设置为只触发一次,直到它被重置(每天),否则每 15 分钟的通知会很快变得烦人!
然后选择如何收到通知,我将其设置为电子邮件和短信,然后创建通知:
图> 图>
结论:
我现在可以部署真正的智能盒子并且(希望)忘记它。当库存水平变低时,我会收到通知,我可以在仪表板上查看它的表现。使用 Sigfox 除了偶尔更换电池之外,我不必担心为设备供电,并且不需要现场设置 WiFi,这使得部署变得非常简单。
我计划将此装置部署到我们位于 Cambridge Makespace 的一个长丝存储箱中,以监控长丝库存水平。
图>
需要解决的问题:
毋庸置疑,这不是一个生产质量完成的项目,还有几个问题需要解决:
Sigfox 下行链路:
Sigfox 允许每天发送 4 条消息以响应上行链路消息。真正智能盒子使用这些来允许重新归零秤,设置上下温度和湿度范围以及物品重量。然而,在尝试使其工作时,即使下行链路消息似乎正在发送并被确认(如 Sigfox 后端所示),Arduino 仍报告 62 状态错误,该错误未映射到任何错误标志为 ATA8520 芯片列出的条件,挖掘驱动程序命令使用的下行链路请求也未在数据表中列出,因此需要做更多的调查。
仅调试:
在禁用调试的情况下运行 Sigfox 通信会导致 Arduino 的低功耗设置被激活,并杀死 USB 串行端口。
低功耗模式:
如 Sigfox 调试设置所述,使用 Arduino 的低功耗库会导致 USB 串口掉线,因此此时未启用。
漂移:
没有添加漂移补偿,毫无疑问称重传感器在恒定负载下会漂移。
噪声/非垂直测量:
Real Smart Box 可能位于货车的后面(例如移动清洁工、木匠、水管工等)。最好在平台上添加一个加速度计并在盒子不稳定时跳过测量周期,同样如果盒子不垂直,重量将不会按预期通过称重传感器。
代码
- 真正智能的 Arduino 代码
真正智能的 Arduino 代码Arduino
为 Arduino MKR FOX 1200、HX711、AdaFruit BME280、Arduino Low Power 添加库。使用 Arduino IDE 进行正常编程。// 真正智能盒子// 测量真正智能盒子内容物的重量// 由两张丙烯酸板制成,中间有 2 个称重传感器 // 放置在一个真正的智能盒子中智能盒子。// 还包括一个 BME280,用于测量盒子内的温度和压力。// 作者:Stephen Harrison // 许可证:MIT#include#include #include #include #include // ---------------------------------- ----// I2C端口上的BME280.Adafruit_BME280 bme; // --------------------------------------// HX711 称重传感器放大器。// 0 :D0 - DOUT// 1:D1 - CLK// 128 的初始增益。HX711 scales(0, 1, 128);// 称重传感器阵列。 Index 0 ==Channel A, Index 1 ==Channel B.float gain[] ={128,32};// 校准因子。// 我们使用 y =mx + c (c =offset, m =scaleFactor)./ / 将测量值转换为重量。// 将此设置为称重传感器报告的偏移量。// 没有重量。float offset[] ={0,54940}; // 将此设置为在秤上放置重量时计算出的系数。// 首先设置偏移量,重新闪烁 arduiono 使其生效// 在秤上放置重量并将原始测量值除以weight.// 使用 scaleFactor =测量值 / weight.float scaleFactor[] ={378.f,260.9f};// ---------------------- ----------------// Sigfox// 这是我们发布到 Sigfox 的数据结构。// 从第一个状态字节中将位拆分为 bool 标志,但该字节仍然需要to be // included otherwise humidity becomes the status// firstRun::bool:7 hx711Fault::bool:6 bmeFault::bool:5 temperatureAlarm::bool:4 humidityAlarm::bool:3 weightAlarm::bool:2 lowStock::bool:1 b0::bool:0// status::int:8 humidity::int:8 temperature::int:8 zeroWeight::int:16:little-endian weight::int:16:little-endian itemCount::int:16:little-endiantypedef struct __attribute__ ((packed)) sigfox_message { uint8_t status; // status::uint:8 -> Split to 8 bits // B7 - First run, B6 - HX711 fault, B5 BME280 fault, B4 Temperature alarm, B3 - Humidity alarm, B2 - weight alarm, B1 - Low stock, B0 - spare int8_t humidity; // humidity::int:8 (yes some sensors (HTU21D read -ve humidity) int8_t temperature; // temperature::int:8 (no decimal places). int16_t zeroWeight; // zeroWeight::int:16:little-endian int16_t weight; // weight::int:16:little-endian int16_t itemCount; // itemCount::int:16:little-endian (100x actual item count to allow for 2.01 (as weight won't match exactly) int8_t driftCorrection; // Drift Correction for changes in zero weight applied to the scales. int8_t filler; int8_t lastStatus; // Last sigfox status} SigfoxMessage;// Time the last Sigfox message was published atlong lastPublish =0;// Time the last Sigfox downlink was requested.// Allowed max 4 per day of these.long lastDownlink =0;uint8_t lastSigfoxStatus =0;// --------------------------------------// Application/state// If the fist cycle (after a reset) for the measure/publish// cycle (this is used to request a downlink message from Sigfox).// Note that only 4 of them are allowed per day so becareful// when deploying code.bool isFirstCycle =true;// Application mode// 0:Normal// 1:Calibrationint mode =0;// Which channel should be read during calibration.int calibrate_channel =0;// The last average value measured for each channel.float lastAverage[] ={0,0};// The current weight of the contents of the boxfloat currentWeight =0;// The weight of the units the box will hold.// Updatable via Sigfox downlink message.float unitWeight =238;// Different to tare as it would be a manual// zero'd at a set reading from scales// This will most likely change with drift (time/temperature/etc)// and should be set once the scale is in place but not loaded.// Updatable via Sigfox downlink message.float zeroWeight =0;bool bmeOk =true;bool hx711Ok =true;// Alarms and alarm rangesfloat minTemperature =5.f;float maxTemperature =60.f;float minHumidity =0.f;float maxHumidity =60.f;float maxWeight =10000; // 10kgbool temperatureAlarm =false;bool humidityAlarm =false;bool weightAlarm =false;float currentTemperature =0;float currentHumidity =0;float stockLevel =0;bool lowStock =false;float minStock =5;// Setup the Arduino.void setup() { pinMode(LED_BUILTIN, OUTPUT); //Initialize serial:Serial.begin(9600); // NB:The sensor I'm using (from random eBay seller) // does not use the default address. bmeOk =bme.begin(0x76); if (!bmeOk) { Serial.println("Could not find a valid BME280 sensor!"); } // Delay for USB Serial connect and for the BME's first reading.延迟(5000); Serial.println("Really Smart Box..."); printHeader();}int delayCounter =0;void loop() { switch (mode) { case 0:measureAndPublish(); //Sleep for 1 minutes // Causing problems with USB connected. //LowPower.sleep(1 * 60 * 1000); delay(60 * 1000);休息; case 1:calibrate();延迟(1000);休息; } // Check for user input via the serial port. checkSerial(); // measure is done on RTC timer tick (once per minute) delay(100);}void measureAndPublish() { // turn the LED on to indicate measuring. digitalWrite(LED_BUILTIN, HIGH); printBmeValues(); measureTemperatureAndHumidity(); measureWeight(true); // Weight, temperature and humidity are read every minute // however we only publish occasionally. if (shouldPublish()) { publishMeasurements(); } digitalWrite(LED_BUILTIN, LOW); }// Main measurement loop. Reads the weight from the load cells// and stores if no noise from the previous read.void measureWeight(bool printDetails) { scales.power_up();延迟(500); float delta =readDelta(printDetails); if (printDetails) { Serial.print("\t"); Serial.print(delta, 2); } // If the delta is between -1 and 1 (i.e. no noise) // update the change in overall weight and units contained // otherwise ignore and try again later on. // This ensures we use only stable readings when both channels have not changed for // two sets of measurements. if (delta <1.f &&delta> -1.f) { // Remember the previous measured weight so we can get a delta. float lastWeight =currentWeight; // Compute the weight. Take the weight of both load cells // added together then subtract the zero'd weight. currentWeight =lastAverage[0] + lastAverage[1] - zeroWeight; // Compute the difference in weight of the items in the box // compated to the last time we had a stable reading. float itemsWeightDelta =currentWeight - lastWeight; updateStockLevels(); if (printDetails) { Serial.print("\t"); Serial.print("\t"); Serial.print(currentWeight, 2); Serial.print("\t"); // divide by unit weight to estimate the stock level in the box Serial.print(currentWeight / unitWeight, 2); Serial.print("\t"); // the change in weight, (i.e. the weight if the items added) Serial.print(itemsWeightDelta, 2); Serial.print("\t"); // divide by unit weight to estimate the units removed/added Serial.print(itemsWeightDelta / unitWeight, 2); } } checkWeightLimits(); if (printDetails) { Serial.println(); } // put the ADC in sleep mode and switch // off the LED now we're done measuring. scales.power_down(); }void updateStockLevels() { stockLevel =currentWeight / unitWeight; // Unlike other alarms the low stock level // is reset if the stock is re-stocked. lowStock =stockLevel maxWeight ) { weightAlarm =true; } if (lastAverage[0]> (maxWeight /2)) { weightAlarm =true; } if (lastAverage[1]> (maxWeight /2)) { weightAlarm =true; }}// Read the difference in weight from the last // average to this time across both load cells.// average value is stored in the lastAverage array.float readDelta(bool printDetails) { float aDelta =readChannel(0, true); if (printDetails) { Serial.print("\t"); } float bDelta =readChannel(1, true); return aDelta + bDelta;}// Read the weight from a channel. Stores the measured value in // the lastAverage array and retuns the delta of the measured value// from the previous lastAverage. This allows us to know if the weight// has changed.// channel 0 =A// channel 1 =Bfloat readChannel(int channel, bool printDetails) { // Gain:// Channel A supports 128 or 64. Default 128 // Channel B supports 32 // Select Channel B by using gain of 32. scales.set_gain(gain[channel]); // HX711 library only has one set of offset/scale factors // which won't work for use as we use both channels and they // have different gains, so each needs to have it's offset/scale set // before reading the value. scales.set_offset(offset[channel]); scales.set_scale(scaleFactor[channel]); // force read to switch to gain. scales.read(); scales.read(); float singleRead =scales.get_units(); float average =scales.get_units(10); float delta =average - lastAverage[channel]; if (printDetails) { Serial.print(singleRead, 1); Serial.print("\t"); Serial.print(average, 1); Serial.print("\t"); Serial.print(delta, 1); Serial.print("\t"); } lastAverage[channel] =average; return delta;}// print the header for the debug data pushed out when measuring.void printHeader() { Serial.print("BME280\t\t\t\t\t"); Serial.print("Channel A\t\t\t"); Serial.print("Channel B\t\t\t"); Serial.print("\t\t"); Serial.print("Totals \t\t\t"); Serial.println(""); Serial.print("Temp\t"); Serial.print("Pressure\t"); Serial.print("Humidity\t"); Serial.print("read\t"); Serial.print("average\t"); Serial.print("delta\t"); Serial.print("\t"); Serial.print("read\t"); Serial.print("average\t"); Serial.print("delta\t"); Serial.print("\t"); Serial.print("sum\t"); Serial.print("\t"); Serial.print("weight\t"); Serial.print("items\t"); Serial.print("change\t"); Serial.print("items added"); Serial.println("");}// Calibration - reads/prints selected channel values.void calibrate() { digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level) scales.set_gain(gain[calibrate_channel]); scales.set_offset(offset[calibrate_channel]); scales.set_scale(scaleFactor[calibrate_channel]); // force read to switch to gain Serial.print("\t|CH:\t"); Serial.print(calibrate_channel,1); Serial.print("\traw:\t"); Serial.print(scales.read(),1); Serial.print("\t| raw:\t"); Serial.print(scales.read(),1); Serial.print("\t| units:\t"); Serial.print(scales.get_units(), 1); Serial.print("\t| gain:\t"); Serial.print(gain[calibrate_channel], 1); Serial.print("\t| factor:\t"); Serial.println(scaleFactor[calibrate_channel], 1); digitalWrite(LED_BUILTIN, LOW);}// check the serial port for input from a console to allow us to alter // the device mode etc.void checkSerial() { if(Serial.available()) { char instruction =Serial.read(); switch (instruction) { case '0':calibrate_channel =0; Serial.println("Channel 0 (A) Selected");休息; case '1':calibrate_channel =1; Serial.println("Channel 1 (B) Selected");休息; case 'm':// Measurement mode mode =0; Serial.println("Measurement Mode"); printHeader();休息; case 'c':// Calibration mode mode =1; Serial.println("Calibration Mode");休息; case 't':// Tare. Teset the scale to 0 Serial.println("Taring"); scales.power_up();延迟(500); scales.tare(5); // Need to do this for each channel // and update our stored offset. Serial.println("Not properly Tared!");休息; case 'h':printHeader();休息; case 'z':zeroScales();休息; case 's':printSigfoxModelDetails();休息; default:Serial.println("Unknown instruction. Select:0, 1, m, c, t, h, z, or s"); Serial.println("m - measurement mode"); Serial.println("c - Calibration mode"); Serial.println("0 - Channel 0 (A) Calibration"); Serial.println("1 - Channel 1 (B) Calibration"); Serial.println("t - Tare (scale)"); Serial.println("z - Zero (Weight)"); Serial.println("h - print Header"); Serial.println("s - print Sigfox model details");休息; } }}// Measure (and record) the temperature and humidity levels// Sets alarms if out of rage (we can't use limits on Internet service side// as the messages may only be sent a few times a day and a brief (maybe hours)// out of range temperature/humidity could easily be missed between// message publishing.void measureTemperatureAndHumidity() { if (!bmeOk) { return; } currentTemperature =bme.readTemperature(); if (currentTemperature maxTemperature) { temperatureAlarm =true; } currentHumidity =bme.readHumidity(); if (currentHumidity maxHumidity) { humidityAlarm =true; }}// Print the values read from the BME280 sensorvoid printBmeValues() { //Serial.print("Temperature ="); Serial.print(bme.readTemperature(), 1); Serial.print("\t"); Serial.print(bme.readPressure() / 100.0F, 0); Serial.print("\t\t"); Serial.print(bme.readHumidity(),1); Serial.print("\t\t ");}// =============================================================// Sigfox handing// =============================================================// Determine if we should publish the Sigfox message.// We may also wish to publish if the stock level has// changed (or a significant weight level has changed)// but we would need to be careful of exceeding the // 140 messages per day for a noisy system.bool shouldPublish() { // Publish every 15 minutes // this doesn't really need to be this often // but whilst developing it helps keep an eye on the system. int messageIntervalMinutes =15; // On first run after reset // allow a 2 minute delay for the platform to be placed into // the box and stabalise before doing first publish // which is also expected to include a check for zeroing the platform. if (isFirstCycle) { messageIntervalMinutes =2; Serial.println("First cycle"); } // How long ago we last publish a Sigfox message long millisAgo =millis() - lastPublish; return millisAgo> (messageIntervalMinutes * 60 * 1000);}// Publish our measurements (weight, temperature, humidity etc)// to Sigfox.void publishMeasurements() { Serial.println("Sending via Sigfox..."); bool useDownlink =shouldUseDownlink(); if (useDownlink) { Serial.println("Using Sigfox downlink..."); } // stub for message which will be sent SigfoxMessage msg =buildMessage(); SigFox.begin(); SigFox.debug(); // Wait at least 30mS after first configuration (100mS before) delay(100); // Clears all pending interrupts SigFox.status();延迟(1); SigFox.beginPacket(); SigFox.write((uint8_t*)&msg, 12); // endPacket actually sends the data. uint8_t statusCode =SigFox.endPacket(useDownlink); printSigfoxStatus(statusCode); // Status =0 for a successful send, otherwise indicates // a failure. // Store when we last published a Sigfox message // to allow for timed message sending. if (statusCode ==0) { resetAlarms(); } // Update the last publish/downlink times // even if an error resonse was received to prevent // repeated publishing lastPublish =millis(); isFirstCycle =false; if (useDownlink) { parseDownlinkData(statusCode); lastDownlink =lastPublish; } // Store the status value lastSigfoxStatus =statusCode; SigFox.end();}void printSigfoxStatus(uint8_t statusCode) { Serial.print("Response status code :0x"); Serial.println(statusCode, HEX); if (statusCode !=0) { Serial.print("Sigfox Status:"); Serial1.println(SigFox.status(SIGFOX)); Serial1.println(); Serial.print("Atmel Status:"); Serial1.println(SigFox.status(ATMEL)); Serial1.println(); }}// Create the message to be publish to Sigfox.SigfoxMessage buildMessage() { SigfoxMessage message; message.status =getStatusFlags(); message.humidity =(int8_t )currentHumidity; message.temperature =(int8_t)currentTemperature; message.zeroWeight =(int16_t)zeroWeight; message.weight =(int16_t)currentWeight; message.itemCount =(int16_t)(stockLevel * 100); message.driftCorrection =0; // TODO message.filler =0; message.lastStatus =lastSigfoxStatus; return message;}// Get the status flags for the Sigfox message.byte getStatusFlags() { byte status =0; // B7 - First run, // B6 - HX711 fault // B5 - BME280 fault // B4 - Temperature alarm // B3 - Humidity alarm // B2 - weight alarm // B1 - Low stock // B0 - spare // Upper Nibble (Charging/Battery) // Battery flat if (isFirstCycle) { status |=0x80; // 1000 0000 } // HX711 fault. // we don't have a way to check this yet. if (!hx711Ok) { status |=0x40; // 0100 0000 } // BME280 fault if (!bmeOk) { status |=0x20; // 0010 0000 } // Over/Under temperature alarm if (temperatureAlarm> 0) { status |=0x10; // 0001 0000 } // Over/Under humidity alarm if (humidityAlarm) { status |=0x08; // 0000 1000 } // Over/under? weight alarm if (weightAlarm) { status |=0x04; // 0000 0100 } // if computed stock level low. if (lowStock) { status |=0x02; // 0000 0010 } return status;}// Determine if we are requesting a downlink message.bool shouldUseDownlink() { // When debugging uncomment this so as to not keep requesting // downlink //return false; // On first run we want to request a downlink // message to help with zero'ing and setup. if (isFirstCycle) { return true; } // How long ago we last did a downlink message. long millisAgo =millis() - lastDownlink; // try every 12 hours, this keeps us under the // maximum 4 per day. return millisAgo> (12 * 60 * 60 * 1000);}// Parse downlinked data.void parseDownlinkData(uint8_t statusMessage) { if (statusMessage> 0) { Serial.println("No transmission. Status:" + String(statusMessage));返回; } // Max response size is 8 bytes // set-up a empty buffer to store this. (0x00 ==no action for us.) uint8_t response[] ={0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // Expect... // Byte 0:Flags // B7:Zero scales // B6:Set Temperature range (ignore min/max temp if 0) // B5:Set Humidity range (ignore min/max humidity if 0) // B4:Set tolerance? // B3:Set ??? // B2:Update unit weight (ignore update if 0) // B1:// B0:// Byte 1:Min T // Byte 2:Max T // Byte 3:Min Humidity // byte 4:Max Humidity // byte 5:Read tolerence??? (+/- x) // byte 6 &7:Unit weight // Parse the response packet from Sigfox if (SigFox.parsePacket()) { Serial.println("Response from server:"); // Move the response into local buffer. int i =0; while (SigFox.available()) { Serial.print("0x"); int readValue =SigFox.read(); Serial.println(readValue, HEX); response[i] =(uint8_t)readValue;我++; } // byte 0 - flags. // 0b 1000 0000 if (response[0] &0x80 ==0x80) { zeroScales(); } // 0b 0100 0000 if (response[0] &0x40 ==0x40) { updateTemperatureAlarm(response[1], response[2]); } // 0b 0010 0000 if (response[0] &0x20 ==0x20) { updateHumidityAlarm(response[3], response[4]); } // 0b 0000 0100 if (response[0] &0x04 ==0x04) { // Little Endian format. (ff dd -> 0xddff uint16_t weight =response[7] <<8 &response[6]; updateUnitWeight(weight); } } else { Serial.println("No response from server"); } Serial.println();}void printSigfoxModelDetails() { if (!SigFox.begin()) { Serial.println("Shield error or not present!"); return; } // Output the ID and PAC needed to register the // device at the Sigfox backend. String version =SigFox.SigVersion(); String ID =SigFox.ID(); String PAC =SigFox.PAC(); // Display module informations Serial.println("MKRFox1200 Sigfox configuration"); Serial.println("SigFox FW version " + version); Serial.println("ID =" + ID); Serial.println("PAC =" + PAC); Serial.println(""); Serial.print("Module temperature:"); Serial.println(SigFox.internalTemperature()); Serial.println("Register your board on https://backend.sigfox.com/activate with provided ID and PAC"); delay(100); // Send the module to the deepest sleep SigFox.end();}// =============================================================// General helper methods// =============================================================// Reset the alarms after they have been published.void resetAlarms() { temperatureAlarm =false; humidityAlarm =false; weightAlarm =false;}void zeroScales() { zeroWeight =lastAverage[0] + lastAverage[1]; Serial.print("Zero'd:"); Serial.print(zeroWeight, 1); Serial.println();}void updateTemperatureAlarm(int8_t lower, int8_t upper) { Serial.print("Setting temperature alarm. Min:"); Serial.print(lower); Serial.print(", Max:"); Serial.println(upper); minTemperature =lower; maxTemperature =upper;}void updateHumidityAlarm(int8_t lower, int8_t upper) { Serial.print("Setting humidity alarm. Min:"); Serial.print(lower); Serial.print(", Max:"); Serial.println(upper); minHumidity =lower; maxHumidity =upper;}void updateUnitWeight(uint16_t weight) { Serial.print("Setting unit weight:"); Serial.println(weight); unitWeight =weight;}
Really Smart Box Github Repository
https://github.com/Tinamous/ReallySmartBox定制零件和外壳
Use this to cut the top and bottom acrylic sheets. cuttingguide_e7GNHf980M.svgThis sits between the lower acrylic sheet and load cell to raise it up a little and provide a edge to the platformsPrint 4 of these for each corner of the lower sheet if needed示意图
Nothing to complex.制造工艺