From 648b4da9da49b9c78eb958eb675219798532f9f6 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Sat, 5 Aug 2017 15:51:54 +0800 Subject: [PATCH] integrate ethmac/smoltcp (timestamp missing), add HTTP server --- firmware/src/board.rs | 10 +++ firmware/src/http.rs | 151 ++++++++++++++++++++++++++++++++++++++ firmware/src/index.html | 50 +++++++++++++ firmware/src/logo.svg.gz | Bin 0 -> 2230 bytes firmware/src/main.rs | 112 +++++++++++++++++++++++++++- firmware/src/pages.rs | 27 +++++++ firmware/src/style.css.gz | Bin 0 -> 4910 bytes 7 files changed, 347 insertions(+), 3 deletions(-) create mode 100644 firmware/src/http.rs create mode 100644 firmware/src/index.html create mode 100644 firmware/src/logo.svg.gz create mode 100644 firmware/src/pages.rs create mode 100644 firmware/src/style.css.gz diff --git a/firmware/src/board.rs b/firmware/src/board.rs index 0d3fb9c..08f9fb4 100644 --- a/firmware/src/board.rs +++ b/firmware/src/board.rs @@ -320,3 +320,13 @@ pub fn init() { adc0.actss.write(|w| w.asen0().bit(true)); }); } + +pub fn get_mac_address() -> [u8; 6] { + let (userreg0, userreg1) = cortex_m::interrupt::free(|cs| { + let flashctl = tm4c129x::FLASH_CTRL.borrow(cs); + (flashctl.userreg0.read().bits(), + flashctl.userreg1.read().bits()) + }); + [userreg0 as u8, (userreg0 >> 8) as u8, (userreg0 >> 16) as u8, + userreg1 as u8, (userreg1 >> 8) as u8, (userreg1 >> 16) as u8] +} diff --git a/firmware/src/http.rs b/firmware/src/http.rs new file mode 100644 index 0000000..4ec7a84 --- /dev/null +++ b/firmware/src/http.rs @@ -0,0 +1,151 @@ +use core::fmt; + +const MAX_PATH: usize = 128; + +#[derive(Debug,Clone,Copy,PartialEq,Eq)] +enum State { + WaitG, + WaitE, + WaitT, + WaitSpace, + GetPath, + WaitCR1, + WaitLF1, + WaitCR2, + WaitLF2, + Finished +} + +pub struct Request { + state: State, + path_idx: usize, + path: [u8; MAX_PATH] +} + +impl Request { + pub fn new() -> Request { + Request { + state: State::WaitG, + path_idx: 0, + path: [0; MAX_PATH] + } + } + + pub fn reset(&mut self) { + self.state = State::WaitG; + self.path_idx = 0; + } + + pub fn input_char(&mut self, c: u8) -> Result { + match self.state { + State::WaitG => { + if c == b'G' { + self.state = State::WaitE; + } else { + return Err("invalid character in method") + } + } + State::WaitE => { + if c == b'E' { + self.state = State::WaitT; + } else { + return Err("invalid character in method") + } + } + State::WaitT => { + if c == b'T' { + self.state = State::WaitSpace; + } else { + return Err("invalid character in method") + } + } + State::WaitSpace => { + if c == b' ' { + self.state = State::GetPath; + } else { + return Err("invalid character in method") + } + } + State::GetPath => { + if c == b'\r' || c == b'\n' { + return Err("GET ended prematurely") + } else if c == b' ' { + if self.path_idx == 0 { + return Err("path is empty") + } else { + self.state = State::WaitCR1; + } + } else { + if self.path_idx >= self.path.len() { + return Err("path is too long") + } else { + self.path[self.path_idx] = c; + self.path_idx += 1; + } + } + } + State::WaitCR1 => { + if c == b'\r' { + self.state = State::WaitLF1; + } + } + State::WaitLF1 => { + if c == b'\n' { + self.state = State::WaitCR2; + } else { + self.state = State::WaitCR1; + } + } + State::WaitCR2 => { + if c == b'\r' { + self.state = State::WaitLF2; + } else { + self.state = State::WaitCR1; + } + } + State::WaitLF2 => { + if c == b'\n' { + self.state = State::Finished; + return Ok(true) + } else { + self.state = State::WaitCR1; + } + } + State::Finished => return Err("trailing characters") + } + Ok(false) + } + + pub fn input(&mut self, buf: &[u8]) -> Result { + let mut result = Ok(false); + for c in buf.iter() { + result = self.input_char(*c); + if result.is_err() { + return result; + } + } + result + } + + pub fn get_path<'a>(&'a self) -> Result<&'a [u8], &'static str> { + if self.state != State::Finished { + return Err("request is not finished") + } + Ok(&self.path[..self.path_idx]) + } +} + +pub fn write_reply_header(output: &mut fmt::Write, status: u16, content_type: &str, gzip: bool) -> fmt::Result { + let status_text = match status { + 200 => "OK", + 404 => "Not Found", + 500 => "Internal Server Error", + _ => return Err(fmt::Error) + }; + write!(output, "HTTP/1.1 {} {}\r\nContent-Type: {}\r\n", + status, status_text, content_type)?; + if gzip { + write!(output, "Content-Encoding: gzip\r\n")?; + } + write!(output, "\r\n") +} diff --git a/firmware/src/index.html b/firmware/src/index.html new file mode 100644 index 0000000..a75e298 --- /dev/null +++ b/firmware/src/index.html @@ -0,0 +1,50 @@ + + +ionpak + + + + + + + + +
+ +
+

Measure

+
+ +
+

Pressure:

+
+
+2.3×10-7 mbar +
+ +
+

+At local time: + +

+
+ +
+ + + + + + + + diff --git a/firmware/src/logo.svg.gz b/firmware/src/logo.svg.gz new file mode 100644 index 0000000000000000000000000000000000000000..135b4b1fc3c06fbf9771b05d97169edf27980c64 GIT binary patch literal 2230 zcmV;n2ub%JiwFqkk9}DH18i?+Z!U9oX8^TWZEvJT68`RALD&zQC<2-Q`<^ftf*OU;@}<@2|h=rfFbiR>?&Ul4W?Ry1J_Rsj9Z`etJ1n!E@1eWm9iL z9HTHO>a5AjdbbJx{pE9{!l3KZI!~*nE;eD^gg@Qi+-Fh$ic1H^O}ktF5Jb`a&COl+yt@g40QRlBB+oXXTX1}8E324i>!Kx^un!2+q>+ao5xy`p;+jw)LRtamZ*NChMi9jRjUh6)6 ziIz*j$7@RoLTC-LN%PNXB^@O62tQtZ1mdoFYO`Vs>x#H8`t_$TpS&x{7u(LL5BAL$Y(TX`|a<@$@pS*3^M~AfC zm37oNNAUR)uZnGdRnQIuyCQjL`rc;X+`&^>L&hRUS**#oG(3B;xX7+kdHJ?|NF!C> zkR8&O@=$&)AQ58t158vfk0!`qM_V?uhSY!Ifk5Ke?>q)Rs?q83^WOOjw7DG3=+EVlEP?|;SU;47D zem0x9$Yw^%zAEnRp2O=H#oC48f?Us{yjzcHuwT2ggjVT8QEkG17?uV2Jl(stc{(1N zyl^}Wr{pep*|%xknKW!d`&z-^`5uZOCYmrLR*~Q_R}4w&M`_{)YnB7+98|7)z#LGKdj}WEE`%B1e>5*`GO`p7)k0!m`37$c1PGaNG?1Zi8Qk<0tgQN!~@G*JKc z9U=j4Rks#-HY@9Xft{?w3kb4N^w~binmRA)uE-q>^^#x&-cooFIlkOhWnH)`O++j( z0~ymGunV^7p{!n$_n)8or#20K{ZKuAtb-3<-rq(eL88I(vB=78nZdko>P!7q@Nx9y z{UVU4Dt9U97TvWlBiP`&poR|Gmf0xliR20$~bqjaSGBk-(g; zWvZA0ru$Ea7whRuycyUu*TjkfEs^vn!|aT^JAKe zIqXMxQ`gXNnl^%A{%k33=G@fIubn={vCwdH;wy<`#W*6;p1!GAjWIT%V`x{xq#hp1 z<_OUY4#Auq7aYBT7@H z?Da6)es5TXQC6XJJSc^9IEP$DiZ1TmA*fHZMT zyLbpoiojE3#gmIpKE>uei&q5^9={ZJaPNkQ!;2z_#022Nw{A5kPFA z5imLgG-2Q~r>=mAgc>oWG|Dp;s(s9Hxgk>BtYsVK5Rzl&vGz z%WWNbjyZyi(jh*gEkrG4EECQ6HFaTVLBs-*P>6VFo+cfX&6_8K2g`Z$bjbDQITMb} zb7{f0%4r+l9jOpdl|wd##8TTxu;egZt$Yea4t9YyQrbv3lQ5njMI@$*K*p~X$#5$i z{!8)yRRwvws?cUOW6rKr1%`}7QgG7?M=L))=(F`ylJEvB@jb8M&pTLSYt45J!ugR?8l+=jcaz@L~5 z;Thy-9tzwX)GJ5zMJQ&9jJ0kMOMRCiNd?l=Wm^2fW%{od1bd@`_-DWwbTSU!&Mp%F z34|~lfy=|9|8eM86f+T2fxF_`EW>a%S+c5d%YaK|Wp87Qz?=sX!Ji`nNsv-TxiYOu z3qui@`ThValRqtmP}U?|_n20;pSagIP2XSnzwXSxvF>mF2X?>KlVunH E09jWrCIA2c literal 0 HcmV?d00001 diff --git a/firmware/src/main.rs b/firmware/src/main.rs index 8dc35f6..36224aa 100644 --- a/firmware/src/main.rs +++ b/firmware/src/main.rs @@ -12,6 +12,11 @@ use cortex_m::exception::Handlers as ExceptionHandlers; use cortex_m::interrupt::Mutex; use tm4c129x::interrupt::Interrupt; use tm4c129x::interrupt::Handlers as InterruptHandlers; +use smoltcp::Error; +use smoltcp::wire::{EthernetAddress, IpAddress}; +use smoltcp::iface::{ArpCache, SliceArpCache, EthernetInterface}; +use smoltcp::socket::{AsSocket, SocketSet}; +use smoltcp::socket::{TcpSocket, TcpSocketBuffer}; #[macro_export] macro_rules! print { @@ -34,6 +39,8 @@ mod pid; mod loop_anode; mod loop_cathode; mod electrometer; +mod http; +mod pages; static TIME: Mutex> = Mutex::new(Cell::new(0)); @@ -68,6 +75,26 @@ impl fmt::Write for UART0 { } } +const TCP_RX_BUFFER_SIZE: usize = 256; +const TCP_TX_BUFFER_SIZE: usize = 8192; + + +macro_rules! create_socket_storage { + ($rx_storage:ident, $tx_storage:ident) => ( + let mut $rx_storage = [0; TCP_RX_BUFFER_SIZE]; + let mut $tx_storage = [0; TCP_TX_BUFFER_SIZE]; + ) +} + +macro_rules! create_socket { + ($set:ident, $rx_storage:ident, $tx_storage:ident, $target:ident) => ( + let tcp_rx_buffer = TcpSocketBuffer::new(&mut $rx_storage[..]); + let tcp_tx_buffer = TcpSocketBuffer::new(&mut $tx_storage[..]); + let tcp_socket = TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer); + let $target = $set.add(tcp_socket); + ) +} + fn main() { // Enable the FPU unsafe { @@ -132,17 +159,95 @@ fn main_with_fpu() { |_|\___/|_| |_| .__/ \__,_|_|\_\ | | |_| -Ready."#); +"#); + + let hardware_addr = EthernetAddress(board::get_mac_address()); + let mut protocol_addrs = [IpAddress::v4(192, 168, 69, 1)]; + println!("MAC {} IP {}", hardware_addr, protocol_addrs[0]); + let mut arp_cache_entries: [_; 8] = Default::default(); + let mut arp_cache = SliceArpCache::new(&mut arp_cache_entries[..]); + ethmac::init(hardware_addr.0); + let mut device = ethmac::EthernetDevice; + let mut iface = EthernetInterface::new( + &mut device, &mut arp_cache as &mut ArpCache, + hardware_addr, &mut protocol_addrs[..]); + + create_socket_storage!(tcp_rx_storage0, tcp_tx_storage0); + create_socket_storage!(tcp_rx_storage1, tcp_tx_storage1); + create_socket_storage!(tcp_rx_storage2, tcp_tx_storage2); + create_socket_storage!(tcp_rx_storage3, tcp_tx_storage3); + create_socket_storage!(tcp_rx_storage4, tcp_tx_storage4); + create_socket_storage!(tcp_rx_storage5, tcp_tx_storage5); + create_socket_storage!(tcp_rx_storage6, tcp_tx_storage6); + create_socket_storage!(tcp_rx_storage7, tcp_tx_storage7); + + let mut socket_set_entries: [_; 8] = Default::default(); + let mut sockets = SocketSet::new(&mut socket_set_entries[..]); + + create_socket!(sockets, tcp_rx_storage0, tcp_tx_storage0, tcp_handle0); + create_socket!(sockets, tcp_rx_storage1, tcp_tx_storage1, tcp_handle1); + create_socket!(sockets, tcp_rx_storage2, tcp_tx_storage2, tcp_handle2); + create_socket!(sockets, tcp_rx_storage3, tcp_tx_storage3, tcp_handle3); + create_socket!(sockets, tcp_rx_storage4, tcp_tx_storage4, tcp_handle4); + create_socket!(sockets, tcp_rx_storage5, tcp_tx_storage5, tcp_handle5); + create_socket!(sockets, tcp_rx_storage6, tcp_tx_storage6, tcp_handle6); + create_socket!(sockets, tcp_rx_storage7, tcp_tx_storage7, tcp_handle7); + + let mut sessions = [ + (http::Request::new(), tcp_handle0), + (http::Request::new(), tcp_handle1), + (http::Request::new(), tcp_handle2), + (http::Request::new(), tcp_handle3), + (http::Request::new(), tcp_handle4), + (http::Request::new(), tcp_handle5), + (http::Request::new(), tcp_handle6), + (http::Request::new(), tcp_handle7), + ]; let mut next_blink = 0; let mut next_info = 0; let mut led_state = true; let mut latch_reset_time = None; loop { - board::process_errors(); - let time = get_time(); + for &mut(ref mut request, ref tcp_handle) in sessions.iter_mut() { + let socket: &mut TcpSocket = sockets.get_mut(*tcp_handle).as_socket(); + if !socket.is_open() { + socket.listen(80).unwrap() + } + + if socket.may_recv() { + let request_status = { + let data = socket.recv(TCP_RX_BUFFER_SIZE).unwrap(); + request.input(data) + }; + match request_status { + Ok(true) => { + if socket.can_send() { + pages::serve(socket, &request); + } + request.reset(); + socket.close(); + } + Ok(false) => (), + Err(err) => { + println!("failed HTTP request: {}", err); + request.reset(); + socket.close(); + } + } + } else if socket.may_send() { + request.reset(); + socket.close(); + } + } + let timestamp_ms = 0; // TODO + match iface.poll(&mut sockets, timestamp_ms) { + Ok(()) | Err(Error::Exhausted) => (), + Err(e) => println!("poll error: {}", e) + } + if time > next_blink { led_state = !led_state; next_blink = time + 1000; @@ -170,6 +275,7 @@ Ready."#); next_info = next_info + 3000; } + board::process_errors(); if board::error_latched() { match latch_reset_time { None => { diff --git a/firmware/src/pages.rs b/firmware/src/pages.rs new file mode 100644 index 0000000..cf2a162 --- /dev/null +++ b/firmware/src/pages.rs @@ -0,0 +1,27 @@ +use core::fmt::Write; +use smoltcp::socket::TcpSocket; +use http; + +pub fn serve(output: &mut TcpSocket, request: &http::Request) { + match request.get_path().unwrap() { + b"/" => { + let data = include_str!("index.html"); + http::write_reply_header(output, 200, "text/html; charset=utf-8", false).unwrap(); + output.write_str(data).unwrap(); + }, + b"/style.css" => { + let data = include_bytes!("style.css.gz"); + http::write_reply_header(output, 200, "text/css", true).unwrap(); + output.send_slice(data).unwrap(); + }, + b"/logo.svg" => { + let data = include_bytes!("logo.svg.gz"); + http::write_reply_header(output, 200, "image/svg+xml", true).unwrap(); + output.send_slice(data).unwrap(); + }, + _ => { + http::write_reply_header(output, 404, "text/plain", false).unwrap(); + write!(output, "Not found").unwrap(); + } + } +} diff --git a/firmware/src/style.css.gz b/firmware/src/style.css.gz new file mode 100644 index 0000000000000000000000000000000000000000..49da7f8fd3a0f24a2df47e54e679eb198aca5bf2 GIT binary patch literal 4910 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 g9hA1uUKb%7 literal 0 HcmV?d00001