A new C# desktop application to replace the VB6 server. Communicates over WiFi (UDP) with ESP8266-based bin temperature modules (existing TM13 hardware). Firmware is complete.
| Layer | Choice |
|---|---|
| UI | WinForms (.NET Framework 4.8) |
| Database | SQLite via EF6 (EntityFramework + System.Data.SQLite) |
| Charts | OxyPlot (OxyPlot.WindowsForms) |
| Network | UDP (System.Net.Sockets.UdpClient) |
| Async | async/await + CancellationToken |
| Config | Newtonsoft.Json |
BinTempsApp/
├── BinTempsApp.slnx
└── BinTempsApp/
├── BinTempsApp.csproj # .NET Framework 4.8 WinForms
├── Program.cs
├── MainForm.cs # Main MDI or tabbed shell
├── Network/
│ ├── UdpServer.cs # Bind, heartbeat broadcast, receive loop
│ └── PacketParser.cs # Parse raw bytes into typed packets
├── Models/
│ ├── Module.cs
│ ├── Sensor.cs
│ ├── TemperatureRecord.cs
│ └── Packet.cs
├── Data/
│ ├── AppDbContext.cs # EF6 DbContext
│ └── Migrations/
├── Services/
│ ├── TemperatureService.cs
│ └── SensorService.cs
└── Forms/
├── DashboardForm.cs # Live module/connection status
├── TemperatureForm.cs # Temperature data sheet
├── ChartForm.cs # OxyPlot trend charts
├── BinMapForm.cs # Visual bin layout
└── SettingsForm.cs # App settings, sensor management
All packets begin with two fixed bytes: type identifier and protocol version (120). Temperature lo/hi = raw DS18B20 16-bit value (signed, divide by 16.0 for Celsius).
| Byte | Value |
|---|---|
| 0 | 100 |
| 1 | 120 |
| 2 | CRC |
Sent by app on a regular interval (e.g. every 30 seconds) to the subnet broadcast address. Modules track the last heartbeat time. If no heartbeat is received for 90 seconds (~3 missed), the module attempts WiFi reconnect and re-announces itself with PGN 30831.
| Byte | Field |
|---|---|
| 0 | 101 |
| 1 | 120 |
| 2 | Module ID (0x00 = global broadcast) |
| 3 | Command byte |
| 4 | CRC |
Command byte bits:
- bit 0 = send sensor temps (30830)
- bit 1 = send module description (30831)
Module ID 0x00 = global broadcast — all modules respond. Module ID 0x01-0xFE = targeted, module responds immediately. Module ID 0xFF = reserved.
| Byte | Field |
|---|---|
| 0 | 102 |
| 1 | 120 |
| 2-7 | Module MAC (6 bytes, used to identify target) |
| 8 | New Module ID |
| 9-18 | New Description (10 bytes, UTF-8) |
| 19 | CRC |
| Byte | Field |
|---|---|
| 0 | 103 |
| 1 | 120 |
| 2 | Module ID |
| 3-10 | Sensor serial / ROM code (8 bytes) |
| 11 | New user data byte 0 |
| 12 | New user data byte 1 |
| 13 | CRC |
| Byte | Field |
|---|---|
| 0 | 110 |
| 1 | 120 |
| 2 | Module ID |
| 3-10 | Sensor serial / ROM code (8 bytes) |
| 11 | Temp lo (raw DS18B20 low byte) |
| 12 | Temp hi (raw DS18B20 high byte) |
| 13 | Sensor user data byte 0 |
| 14 | Sensor user data byte 1 |
| 15 | Count of sensors remaining |
| 16 | CRC |
One packet per sensor. Sensor user data (bin/cable/sensor encoding) is included inline with each reading. Count of sensors remaining decrements to 0 on the final packet.
| Byte | Field |
|---|---|
| 0 | 111 |
| 1 | 120 |
| 2 | Module ID (0 = unregistered) |
| 3-8 | Module MAC (6 bytes) |
| 9-18 | Module description (10 bytes, UTF-8) |
| 19-20 | InoID / firmware version (uint16, lo byte first) |
| 21 | CRC |
Sent unsolicited once on startup/reconnect. If module ID = 0, server knows it is unregistered and prompts user to assign an ID via 30822. Also sent in response to 30821 command bit 1. The InoID field allows the app to detect outdated firmware.
16-bit value stored in DS18B20 EEPROM bytes 2-3 (unchanged from original firmware):
[15:8] = Bin ID [7:4] = Cable ID [3:0] = Sensor number
public static (byte Bin, byte Cable, byte Sensor) DecodeUserData(ushort raw)
=> ((byte)(raw >> 8), (byte)((raw >> 4) & 0xF), (byte)(raw & 0xF));Modules are physically installed in the field (grain bins), often too far from the server PC to reliably connect to the module's soft-AP hotspot. Provisioning is therefore done via a smartphone brought to the bin.
- Module boots with no stored credentials (or fails to connect after N attempts)
- Module starts a soft-AP named
BinTemps-<MAC>and serves a simple config web page - Technician walks to the bin with a smartphone, connects to the module's AP
- Browser opens the config page, technician enters SSID + password
- Module saves credentials to EEPROM and reboots into station mode
- On reconnect, module sends unsolicited 30831 — the C# app handles registration from there
The C# app has no role in provisioning. It only becomes involved once the module is on the network.
- MAC address is the stable unique key for each module (factory-burned, never changes).
- Module ID (1 byte, 1-254) is assigned by the server via 30822 and stored in module EEPROM.
- ID = 0 means unregistered.
- Module powers up with ID = 0
- Module sends one unsolicited 30831 (MAC + ID=0)
- Server detects ID=0, prompts user to assign name/ID
- Server sends 30822 using MAC as key, assigns new ID + description
- Module stores ID in EEPROM, uses it from then on
- If 30822 is lost, the next global 30821 (broadcast) will re-trigger the 30831 from the module
- Module receives no heartbeat for 90 seconds
- If WiFi is disconnected, module attempts reconnect
- On reconnect,
udp.begin(port)is called and module sends unsolicited 30831 (with its assigned ID) - If WiFi is still connected but server went quiet, module re-sends 30831 to re-announce itself
- Server knows the module is back online
Modules - MacAddress (PK), ModuleId, Name, LastKnownIp, LastSeen, Status
Sensors - RomCode (PK), ModuleMac (FK), BinId, CableId, SensorNum, Enabled, Offset, MaxTemp, Label
Records - Id, RomCode (FK), Temperature, Timestamp
Bins - Id, Name, Description, Rows, Cols
BinSensorMap - BinId, SensorRomCode, Row, Col
Settings - Key, Value
- App binds to
0.0.0.0on port 1600 to receive UDP from any module on the network - Heartbeat (30820) and commands (30821) are sent to the subnet broadcast address
- Module responses (30830, 30831) arrive from the module's IP — use
UdpReceiveResult.RemoteEndPointto track module IPs UdpClientmust haveEnableBroadcast = trueandClient.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true)- Since modules broadcast their responses, the app may receive its own sent packets — filter by checking the source IP is not the local machine
- Phase 1 (start here) —
UdpServer(bind, broadcast heartbeat, receive loop),PacketParserwith unit tests, console test harness against real hardware - Phase 2 — EF Core models (
AppDbContext, migrations),TemperatureService,SensorService - Phase 3 — Main WPF window, Dashboard (live module list, online/offline status), Temperature sheet
- Phase 4 — Chart view, Bin map, CSV export
- Phase 5 — Settings UI, Sensor management (assign bin/cable/sensor), Installer
- Add full timestamp (hour/min/date) to heartbeat 30820 if modules ever need to self-timestamp queued data while server is offline
- ACK/NAK response from module for 30822/30823 write commands (not yet defined)
- Error/diagnostic reporting PGN (1-Wire bus failures, CRC error counts, WiFi RSSI)
- OTA firmware update PGN