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.
# 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]]
name = "aligned"
version = "0.3.1"
@ -300,6 +282,24 @@ dependencies = [
"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]]
name = "typenum"
version = "1.10.0"

View File

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

View File

@ -1,66 +1,21 @@
# Synopsis
Exposes readings from an ADC pin (currently: *PA3*) of the board via a
TCP service on the Ethernet port.
Trivial network-controlled plugs
================================
# Network Protocol
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
Instructions
------------
![Made for NixOS](https://nixos.org/logo/nixos-lores.png)
## Build the firmware with `default.nix`
Build the firmware with `default.nix`
+++++++++++++++++++++++++++++++++++++
* `nix-build`
* 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`
* Spawning `openocd`, the devboard should be connected already.

View File

@ -11,13 +11,13 @@ let
rustPlatform = recurseIntoAttrs (callPackage ./nix/rustPlatform.nix {
inherit rustManifest;
});
adc2tcp = callPackage ./nix/adc2tcp.nix { inherit rustPlatform; };
tnetplug = callPackage ./nix/tnetplug.nix { inherit rustPlatform; };
openocd = callPackage ./nix/openocd.nix {};
in
stdenv.mkDerivation {
name = "adc2tcp-dist";
name = "tnetplug-dist";
buildInputs = [
adc2tcp
tnetplug
openocd
makeWrapper
];
@ -26,7 +26,7 @@ stdenv.mkDerivation {
installPhase =
let
firmwareBinary = "$out/lib/adc2tcp.elf";
firmwareBinary = "$out/lib/tnetplug.elf";
openOcdFlags = [
"-c" "reset halt"
"-c" "flash write_image erase ${firmwareBinary}"
@ -37,9 +37,9 @@ stdenv.mkDerivation {
in ''
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}"
echo file binary-dist ${firmwareBinary} >> $out/nix-support/hydra-build-products

View File

@ -2,33 +2,33 @@
with rustPlatform;
let
sha256 = "1i9p5d5n01ajbp8lmavyway6vr1mmy107qnccff9glvr91rqx352";
sha256 = "19cdc0lkm9247n6kf23ki66gysz530j1x2lfnzq7n0cpcs53q3h3";
fetchcargo = import ./fetchcargo.nix {
inherit stdenv cacert git cargo-vendor;
inherit (rust) cargo;
};
adc2tcpDeps = fetchcargo {
name = "adc2tcp";
tnetplugDeps = fetchcargo {
name = "tnetplug";
src = ../.;
inherit sha256;
};
in
buildRustPackage rec {
name = "adc2tcp";
name = "tnetplug";
version = "0.0.0";
src = ../.;
cargoSha256 = sha256;
buildInputs = [ adc2tcpDeps ];
buildInputs = [ tnetplugDeps ];
patchPhase = ''
cat >> .cargo/config <<EOF
[source.crates-io]
replace-with = "vendored-sources"
[source.vendored-sources]
directory = "${adc2tcpDeps}"
directory = "${tnetplugDeps}"
EOF
'';
@ -40,6 +40,6 @@ buildRustPackage rec {
doCheck = false;
installPhase = ''
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;
let
adc2tcp = callPackage ./default.nix {
tnetplug = callPackage ./default.nix {
inherit rustManifest;
mozillaOverlay = import <mozillaOverlay>;
};
in
{
build = lib.hydraJob adc2tcp;
build = lib.hydraJob tnetplug;
}

View File

@ -10,7 +10,7 @@ let
openocd = callPackage ./nix/openocd.nix {};
in
stdenv.mkDerivation {
name = "adc2tcp-env";
name = "tnetplug-env";
buildInputs = with rustPlatform.rust; [
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_rt::entry;
use embedded_hal::watchdog::{WatchdogEnable, Watchdog};
use embedded_hal::digital::OutputPin;
use stm32f4xx_hal::{
rcc::RccExt,
gpio::GpioExt,
@ -25,20 +26,11 @@ use smoltcp::{
wire::EthernetAddress,
};
mod adc_input;
use adc_input::AdcInput;
mod net;
mod server;
use server::Server;
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"))]
const NET_HWADDR: [u8; 6] = [0x02, 0x00, 0xDE, 0xAD, 0xBE, 0xEF];
@ -67,7 +59,7 @@ fn init_log() {
#[entry]
fn main() -> ! {
init_log();
info!("adc2tcp");
info!("tnetplug");
let mut cp = CorePeripherals::take().unwrap();
cp.SCB.enable_icache();
@ -90,14 +82,11 @@ fn main() -> ! {
let gpioa = dp.GPIOA.split();
let gpiob = dp.GPIOB.split();
let gpioc = dp.GPIOC.split();
let gpioe = dp.GPIOE.split();
let gpiog = dp.GPIOG.split();
let mut led_green = Led::green(gpiob.pb0.into_push_pull_output());
let mut led_blue = Led::blue(gpiob.pb7.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);
let mut relay1 = gpioe.pe11.into_push_pull_output();
let mut relay2 = gpioe.pe9.into_push_pull_output();
info!("Eth setup");
stm32_eth::setup_pins(
@ -120,25 +109,22 @@ fn main() -> ! {
info!("Net startup");
net::run(&mut cp.NVIC, dp.ETHERNET_MAC, dp.ETHERNET_DMA, hwaddr, |iface| {
Server::run(iface, |server| {
let mut last_output = 0_u32;
loop {
let now = timer::now().0;
let instant = Instant::from_millis(i64::from(now));
led_blue.on();
cortex_m::interrupt::free(net::clear_pending);
server.poll(instant)
let cmd = server.poll(instant)
.unwrap_or_else(|e| {
warn!("poll: {:?}", e);
None
});
led_blue.off();
let now = timer::now().0;
if now - last_output >= OUTPUT_INTERVAL {
led_red.on();
let adc_value = adc_input.read();
writeln!(server, "t={},pa3={}\r", now, adc_value).unwrap();
last_output = now;
led_red.off();
match cmd {
Some(b'a') => { relay1.set_low(); writeln!(server, "A=OFF").unwrap(); },
Some(b'A') => { relay1.set_high(); writeln!(server, "A=ON").unwrap(); },
Some(b'b') => { relay2.set_low(); writeln!(server, "B=OFF").unwrap(); },
Some(b'B') => { relay2.set_high(); writeln!(server, "B=ON").unwrap(); },
_ => (),
}
// Update watchdog
@ -146,10 +132,8 @@ fn main() -> ! {
cortex_m::interrupt::free(|cs| {
if !net::is_pending(cs) {
led_green.on();
// Wait for interrupts
wfi();
led_green.off();
}
});
}

View File

@ -43,7 +43,7 @@ pub fn run<F>(
eth_dev.enable_interrupt(nvic);
// 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 neighbor_storage = [None; 16];
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
/// sessions. Many data structures in `Server::run()` correspond to
/// 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> {
net: EthernetInterface<'a, 'a, 'a, &'a mut stm32_eth::Eth<'static, 'static>>,
sockets: SocketSet<'b, 'b, 'static>,
@ -79,7 +77,7 @@ impl<'a, 'b> Server<'a, 'b> {
}
/// 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
let mut poll_error = None;
let activity = self.net.poll(&mut self.sockets, now)
@ -88,23 +86,33 @@ impl<'a, 'b> Server<'a, 'b> {
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 {
// Listen on all sockets
for handle in &self.handles {
let mut socket = self.sockets.get::<TcpSocket>(*handle);
if ! socket.is_open() {
let _ = socket.listen(TCP_PORT);
if !socket.is_open() {
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();
}
}
}
// 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),
}
Ok(ret)
}
}