first tnetplug version

master
Sebastien Bourdeauducq 2019-12-01 17:03:02 +08:00
parent 91c27d2499
commit 8b35c4260a
12 changed files with 84 additions and 234 deletions

36
Cargo.lock generated
View File

@ -1,23 +1,5 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
[[package]]
name = "adc2tcp"
version = "0.0.0"
dependencies = [
"bare-metal 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
"cortex-m 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"cortex-m-log 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"cortex-m-rt 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)",
"embedded-hal 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"hash2hwaddr 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"panic-abort 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"panic-semihosting 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
"smoltcp 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"stm32-eth 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"stm32f4xx-hal 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "aligned" name = "aligned"
version = "0.3.1" version = "0.3.1"
@ -300,6 +282,24 @@ dependencies = [
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "tnetplug"
version = "0.1.0"
dependencies = [
"bare-metal 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
"cortex-m 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"cortex-m-log 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"cortex-m-rt 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)",
"embedded-hal 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"hash2hwaddr 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"panic-abort 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"panic-semihosting 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
"smoltcp 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"stm32-eth 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"stm32f4xx-hal 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.10.0" version = "1.10.0"

View File

@ -1,18 +1,13 @@
[package] [package]
categories = ["embedded", "no-std"] categories = ["embedded", "no-std"]
name = "adc2tcp" name = "tnetplug"
description = "Poll ADC pin, report over TCP" description = "Trivial network-controlled plugs"
license = "GPL-3.0-only" license = "GPL-3.0-only"
authors = ["Astro <astro@spaceboyz.net>"] authors = ["Sebastien Bourdeauducq <sb@m-labs.hk>"]
version = "0.0.0" version = "0.1.0"
keywords = ["ethernet", "eth", "stm32", "adc", "tcp"] keywords = ["ethernet", "eth", "stm32", "smartplug", "internetofshit"]
repository = "https://github.com/m-labs/adc2tcp"
edition = "2018" edition = "2018"
[badges]
travis-ci = { repository = "astro/adc2tcp", branch = "master" }
maintenance = { status = "experimental" }
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = [] features = []
default-target = "thumbv7em-none-eabihf" default-target = "thumbv7em-none-eabihf"

View File

@ -1,66 +1,21 @@
# Synopsis Trivial network-controlled plugs
================================
Exposes readings from an ADC pin (currently: *PA3*) of the board via a
TCP service on the Ethernet port.
# Network Protocol Instructions
------------
Sensor readings produce lines of `key=value` pairs, joined by `,`,
terminated by `"\r\n"`.
```
t=21000,pa3=685
t=22000,pa3=684
t=23000,pa3=681
t=24000,pa3=696
t=25000,pa3=673
t=26000,pa3=689
t=27000,pa3=657
t=28000,pa3=654
t=29000,pa3=652
t=30000,pa3=662
t=31000,pa3=663
```
| Key | Value | Unit |
|:---:|-------------|------|
| t | Time | ms |
| pa3 | ADC reading | mV |
# LEDs
Colors indicate what the MCU is occupied with.
| Color | Indication |
|:-------:|-------------------|
| Green | WFI (idle) |
| Blue | Network poll |
| Red | Message broadcast |
# Crate features
* `semihosting` enables log output via the **cortex-m-semihosting**
crate. Use only in development! MCU will hang when no OpenOCD is
running.
* `generate-hwaddr` generates an Ethernet MAC address by hashing the
unique device ID from flash memory.
# Instructions
![Made for NixOS](https://nixos.org/logo/nixos-lores.png) ![Made for NixOS](https://nixos.org/logo/nixos-lores.png)
## Build the firmware with `default.nix` Build the firmware with `default.nix`
+++++++++++++++++++++++++++++++++++++
* `nix-build` * `nix-build`
* This uses **cargo-vendor** to bundle dependencies, so that unstable versions from git can be used. * This uses **cargo-vendor** to bundle dependencies, so that unstable versions from git can be used.
* Run `result/bin/flash-adc2tcp` to flash a devboard with OpenOCD and quit. * Run `result/bin/flash-tnetplug` to flash a devboard with OpenOCD and quit.
## Development environment with `shell.nix` Development environment with `shell.nix`
++++++++++++++++++++++++++++++++++++++++
* `nix-shell` * `nix-shell`
* Spawning `openocd`, the devboard should be connected already. * Spawning `openocd`, the devboard should be connected already.

View File

@ -11,13 +11,13 @@ let
rustPlatform = recurseIntoAttrs (callPackage ./nix/rustPlatform.nix { rustPlatform = recurseIntoAttrs (callPackage ./nix/rustPlatform.nix {
inherit rustManifest; inherit rustManifest;
}); });
adc2tcp = callPackage ./nix/adc2tcp.nix { inherit rustPlatform; }; tnetplug = callPackage ./nix/tnetplug.nix { inherit rustPlatform; };
openocd = callPackage ./nix/openocd.nix {}; openocd = callPackage ./nix/openocd.nix {};
in in
stdenv.mkDerivation { stdenv.mkDerivation {
name = "adc2tcp-dist"; name = "tnetplug-dist";
buildInputs = [ buildInputs = [
adc2tcp tnetplug
openocd openocd
makeWrapper makeWrapper
]; ];
@ -26,7 +26,7 @@ stdenv.mkDerivation {
installPhase = installPhase =
let let
firmwareBinary = "$out/lib/adc2tcp.elf"; firmwareBinary = "$out/lib/tnetplug.elf";
openOcdFlags = [ openOcdFlags = [
"-c" "reset halt" "-c" "reset halt"
"-c" "flash write_image erase ${firmwareBinary}" "-c" "flash write_image erase ${firmwareBinary}"
@ -37,9 +37,9 @@ stdenv.mkDerivation {
in '' in ''
mkdir -p $out/bin $out/lib $out/nix-support mkdir -p $out/bin $out/lib $out/nix-support
ln -s ${adc2tcp}/lib/adc2tcp ${firmwareBinary} ln -s ${tnetplug}/lib/tnetplug ${firmwareBinary}
makeWrapper ${openocd}/bin/openocd-nucleo-f429zi $out/bin/flash-adc2tcp \ makeWrapper ${openocd}/bin/openocd-nucleo-f429zi $out/bin/flash-tnetplug \
--add-flags "${lib.escapeShellArgs openOcdFlags}" --add-flags "${lib.escapeShellArgs openOcdFlags}"
echo file binary-dist ${firmwareBinary} >> $out/nix-support/hydra-build-products echo file binary-dist ${firmwareBinary} >> $out/nix-support/hydra-build-products

View File

@ -2,33 +2,33 @@
with rustPlatform; with rustPlatform;
let let
sha256 = "1i9p5d5n01ajbp8lmavyway6vr1mmy107qnccff9glvr91rqx352"; sha256 = "19cdc0lkm9247n6kf23ki66gysz530j1x2lfnzq7n0cpcs53q3h3";
fetchcargo = import ./fetchcargo.nix { fetchcargo = import ./fetchcargo.nix {
inherit stdenv cacert git cargo-vendor; inherit stdenv cacert git cargo-vendor;
inherit (rust) cargo; inherit (rust) cargo;
}; };
adc2tcpDeps = fetchcargo { tnetplugDeps = fetchcargo {
name = "adc2tcp"; name = "tnetplug";
src = ../.; src = ../.;
inherit sha256; inherit sha256;
}; };
in in
buildRustPackage rec { buildRustPackage rec {
name = "adc2tcp"; name = "tnetplug";
version = "0.0.0"; version = "0.0.0";
src = ../.; src = ../.;
cargoSha256 = sha256; cargoSha256 = sha256;
buildInputs = [ adc2tcpDeps ]; buildInputs = [ tnetplugDeps ];
patchPhase = '' patchPhase = ''
cat >> .cargo/config <<EOF cat >> .cargo/config <<EOF
[source.crates-io] [source.crates-io]
replace-with = "vendored-sources" replace-with = "vendored-sources"
[source.vendored-sources] [source.vendored-sources]
directory = "${adc2tcpDeps}" directory = "${tnetplugDeps}"
EOF EOF
''; '';
@ -40,6 +40,6 @@ buildRustPackage rec {
doCheck = false; doCheck = false;
installPhase = '' installPhase = ''
mkdir -p $out/lib mkdir -p $out/lib
cp target/thumbv7em-none-eabihf/release/adc2tcp $out/lib/ cp target/thumbv7em-none-eabihf/release/tnetplug $out/lib/
''; '';
} }

View File

@ -5,11 +5,11 @@
with pkgs; with pkgs;
let let
adc2tcp = callPackage ./default.nix { tnetplug = callPackage ./default.nix {
inherit rustManifest; inherit rustManifest;
mozillaOverlay = import <mozillaOverlay>; mozillaOverlay = import <mozillaOverlay>;
}; };
in in
{ {
build = lib.hydraJob adc2tcp; build = lib.hydraJob tnetplug;
} }

View File

@ -10,7 +10,7 @@ let
openocd = callPackage ./nix/openocd.nix {}; openocd = callPackage ./nix/openocd.nix {};
in in
stdenv.mkDerivation { stdenv.mkDerivation {
name = "adc2tcp-env"; name = "tnetplug-env";
buildInputs = with rustPlatform.rust; [ buildInputs = with rustPlatform.rust; [
rustc cargo pkgs.gdb rustc cargo pkgs.gdb
]; ];

View File

@ -1,46 +0,0 @@
use stm32f4xx_hal::{
adc::{
Adc,
config::*,
},
gpio::{Analog, gpioa::PA3 as Pin},
stm32::ADC1 as ADC,
};
/// ADC Input
pub struct AdcInput {
/// unused but consumed
_pin: Pin<Analog>,
adc: Adc<ADC>,
}
impl AdcInput {
/// Configure pin into analog mode
pub fn new<MODE>(adc: ADC, pin: Pin<MODE>) -> Self {
let pin = pin.into_analog();
let adc_config = AdcConfig::default()
.scan(Scan::Enabled)
.continuous(Continuous::Single)
.clock(Clock::Pclk2_div_2);
let mut adc = Adc::adc1(adc, true, adc_config);
adc.configure_channel(&pin, Sequence::One, SampleTime::Cycles_480);
AdcInput { _pin: pin, adc }
}
/// Enable the ADC,
/// run a conversion
/// disable the ADC
pub fn read(&mut self) -> u16 {
let adc = &mut self.adc;
adc.enable();
adc.clear_end_of_conversion_flag();
adc.start_conversion();
let sample = adc.current_sample();
let result = adc.sample_to_millivolts(sample);
adc.wait_for_conversion_sequence();
adc.disable();
result
}
}

View File

@ -1,46 +0,0 @@
use embedded_hal::digital::OutputPin;
use stm32f4xx_hal::gpio::{
Output, PushPull,
gpiob::{PB0, PB7, PB14},
};
type GreenPin = PB0<Output<PushPull>>;
type BluePin = PB7<Output<PushPull>>;
type RedPin = PB14<Output<PushPull>>;
pub struct Led<PIN> {
pin: PIN,
}
impl<PIN: OutputPin> Led<PIN> {
fn new(pin: PIN) -> Self {
Led { pin }
}
pub fn on(&mut self) {
self.pin.set_high();
}
pub fn off(&mut self) {
self.pin.set_low();
}
}
impl Led<GreenPin> {
pub fn green(pin: GreenPin) -> Self {
Self::new(pin)
}
}
impl Led<BluePin> {
pub fn blue(pin: BluePin) -> Self {
Self::new(pin)
}
}
impl Led<RedPin> {
pub fn red(pin: RedPin) -> Self {
Self::new(pin)
}
}

View File

@ -13,6 +13,7 @@ use core::fmt::Write;
use cortex_m::asm::wfi; use cortex_m::asm::wfi;
use cortex_m_rt::entry; use cortex_m_rt::entry;
use embedded_hal::watchdog::{WatchdogEnable, Watchdog}; use embedded_hal::watchdog::{WatchdogEnable, Watchdog};
use embedded_hal::digital::OutputPin;
use stm32f4xx_hal::{ use stm32f4xx_hal::{
rcc::RccExt, rcc::RccExt,
gpio::GpioExt, gpio::GpioExt,
@ -25,20 +26,11 @@ use smoltcp::{
wire::EthernetAddress, wire::EthernetAddress,
}; };
mod adc_input;
use adc_input::AdcInput;
mod net; mod net;
mod server; mod server;
use server::Server; use server::Server;
mod timer; mod timer;
mod led;
use led::Led;
/// Interval at which to sample the ADC input and broadcast to all
/// clients.
///
/// This should be a multiple of the `TIMER_RATE`.
const OUTPUT_INTERVAL: u32 = 1000;
#[cfg(not(feature = "generate-hwaddr"))] #[cfg(not(feature = "generate-hwaddr"))]
const NET_HWADDR: [u8; 6] = [0x02, 0x00, 0xDE, 0xAD, 0xBE, 0xEF]; const NET_HWADDR: [u8; 6] = [0x02, 0x00, 0xDE, 0xAD, 0xBE, 0xEF];
@ -67,7 +59,7 @@ fn init_log() {
#[entry] #[entry]
fn main() -> ! { fn main() -> ! {
init_log(); init_log();
info!("adc2tcp"); info!("tnetplug");
let mut cp = CorePeripherals::take().unwrap(); let mut cp = CorePeripherals::take().unwrap();
cp.SCB.enable_icache(); cp.SCB.enable_icache();
@ -90,14 +82,11 @@ fn main() -> ! {
let gpioa = dp.GPIOA.split(); let gpioa = dp.GPIOA.split();
let gpiob = dp.GPIOB.split(); let gpiob = dp.GPIOB.split();
let gpioc = dp.GPIOC.split(); let gpioc = dp.GPIOC.split();
let gpioe = dp.GPIOE.split();
let gpiog = dp.GPIOG.split(); let gpiog = dp.GPIOG.split();
let mut led_green = Led::green(gpiob.pb0.into_push_pull_output()); let mut relay1 = gpioe.pe11.into_push_pull_output();
let mut led_blue = Led::blue(gpiob.pb7.into_push_pull_output()); let mut relay2 = gpioe.pe9.into_push_pull_output();
let mut led_red = Led::red(gpiob.pb14.into_push_pull_output());
info!("ADC init");
let mut adc_input = AdcInput::new(dp.ADC1, gpioa.pa3);
info!("Eth setup"); info!("Eth setup");
stm32_eth::setup_pins( stm32_eth::setup_pins(
@ -120,25 +109,22 @@ fn main() -> ! {
info!("Net startup"); info!("Net startup");
net::run(&mut cp.NVIC, dp.ETHERNET_MAC, dp.ETHERNET_DMA, hwaddr, |iface| { net::run(&mut cp.NVIC, dp.ETHERNET_MAC, dp.ETHERNET_DMA, hwaddr, |iface| {
Server::run(iface, |server| { Server::run(iface, |server| {
let mut last_output = 0_u32;
loop { loop {
let now = timer::now().0; let now = timer::now().0;
let instant = Instant::from_millis(i64::from(now)); let instant = Instant::from_millis(i64::from(now));
led_blue.on();
cortex_m::interrupt::free(net::clear_pending); cortex_m::interrupt::free(net::clear_pending);
server.poll(instant) let cmd = server.poll(instant)
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
warn!("poll: {:?}", e); warn!("poll: {:?}", e);
None
}); });
led_blue.off();
let now = timer::now().0; match cmd {
if now - last_output >= OUTPUT_INTERVAL { Some(b'a') => { relay1.set_low(); writeln!(server, "A=OFF").unwrap(); },
led_red.on(); Some(b'A') => { relay1.set_high(); writeln!(server, "A=ON").unwrap(); },
let adc_value = adc_input.read(); Some(b'b') => { relay2.set_low(); writeln!(server, "B=OFF").unwrap(); },
writeln!(server, "t={},pa3={}\r", now, adc_value).unwrap(); Some(b'B') => { relay2.set_high(); writeln!(server, "B=ON").unwrap(); },
last_output = now; _ => (),
led_red.off();
} }
// Update watchdog // Update watchdog
@ -146,10 +132,8 @@ fn main() -> ! {
cortex_m::interrupt::free(|cs| { cortex_m::interrupt::free(|cs| {
if !net::is_pending(cs) { if !net::is_pending(cs) {
led_green.on();
// Wait for interrupts // Wait for interrupts
wfi(); wfi();
led_green.off();
} }
}); });
} }

View File

@ -43,7 +43,7 @@ pub fn run<F>(
eth_dev.enable_interrupt(nvic); eth_dev.enable_interrupt(nvic);
// IP stack // IP stack
let local_addr = IpAddress::v4(192, 168, 69, 3); let local_addr = IpAddress::v4(192, 168, 1, 31);
let mut ip_addrs = [IpCidr::new(local_addr, 24)]; let mut ip_addrs = [IpCidr::new(local_addr, 24)];
let mut neighbor_storage = [None; 16]; let mut neighbor_storage = [None; 16];
let neighbor_cache = NeighborCache::new(&mut neighbor_storage[..]); let neighbor_cache = NeighborCache::new(&mut neighbor_storage[..]);

View File

@ -7,7 +7,7 @@ use smoltcp::{
}; };
const TCP_PORT: u16 = 23; const TCP_PORT: u16 = 3131;
/// Number of server sockets and therefore concurrent client /// Number of server sockets and therefore concurrent client
/// sessions. Many data structures in `Server::run()` correspond to /// sessions. Many data structures in `Server::run()` correspond to
/// this const. /// this const.
@ -32,8 +32,6 @@ macro_rules! create_socket {
) )
} }
/// Contains a number of server sockets that get all sent the same
/// data (through `fmt::Write`).
pub struct Server<'a, 'b> { pub struct Server<'a, 'b> {
net: EthernetInterface<'a, 'a, 'a, &'a mut stm32_eth::Eth<'static, 'static>>, net: EthernetInterface<'a, 'a, 'a, &'a mut stm32_eth::Eth<'static, 'static>>,
sockets: SocketSet<'b, 'b, 'static>, sockets: SocketSet<'b, 'b, 'static>,
@ -79,7 +77,7 @@ impl<'a, 'b> Server<'a, 'b> {
} }
/// Poll the interface and the sockets /// Poll the interface and the sockets
pub fn poll(&mut self, now: Instant) -> Result<(), smoltcp::Error> { pub fn poll(&mut self, now: Instant) -> Result<Option<u8>, smoltcp::Error> {
// Poll smoltcp EthernetInterface // Poll smoltcp EthernetInterface
let mut poll_error = None; let mut poll_error = None;
let activity = self.net.poll(&mut self.sockets, now) let activity = self.net.poll(&mut self.sockets, now)
@ -88,23 +86,33 @@ impl<'a, 'b> Server<'a, 'b> {
true true
}); });
// Pass some smoltcp errors to the caller
match poll_error {
None => (),
Some(smoltcp::Error::Malformed) => (),
Some(smoltcp::Error::Unrecognized) => (),
Some(e) => return Err(e),
}
let mut ret = None;
if activity { if activity {
// Listen on all sockets // Listen on all sockets
for handle in &self.handles { for handle in &self.handles {
let mut socket = self.sockets.get::<TcpSocket>(*handle); let mut socket = self.sockets.get::<TcpSocket>(*handle);
if ! socket.is_open() { if !socket.is_open() {
let _ = socket.listen(TCP_PORT); socket.listen(TCP_PORT).unwrap();
}
if socket.may_recv() {
if ret.is_none() {
ret = socket.recv(|data| { if data.len() > 0 { (1, Some(data[0])) } else { (0, None) } }).unwrap();
}
} else if socket.may_send() {
socket.close();
} }
} }
} }
Ok(ret)
// Pass some smoltcp errors to the caller
match poll_error {
None => Ok(()),
Some(smoltcp::Error::Malformed) => Ok(()),
Some(smoltcp::Error::Unrecognized) => Ok(()),
Some(e) => Err(e),
}
} }
} }