├── README.md ├── ap_dhcp ├── config.ml └── unikernel.ml ├── hello_world ├── config.ml └── unikernel.ml └── lcd_wifi_demo ├── config.ml └── unikernel.ml /README.md: -------------------------------------------------------------------------------- 1 | # Mirage unikernel samples for ESP32 chips! 2 | 3 | ## What 4 | 5 | ### hello_world 6 | 7 | Prints "Hello!" every second to demonstrate the most basic Mirage unikernel. 8 | 9 | ### ap_dhcp 10 | 11 | This unikernel creates a wifi access point on which up to 4 devices can connect (ESP32 driver limit). 12 | It embeds the charrua DHCP server to allocate IPs to stations. 13 | 14 | ### lcd_wifi_demo 15 | 16 | This unikernel does a lot of things and is designed for ESP WROVER KIT boards that has an LCD display. 17 | - connects to a specified wifi access point. 18 | - gets an IP via DHCP. 19 | - download a bitmap picture from an ip/port. 20 | - display the picture on the LCD. 21 | - hosts a TCP server that listens for order to move the picture on the screen. 22 | 23 | ## How 24 | 25 | This remains experimental but I've done a lot to make the process as simple as possible. 26 | 27 | * Add my custom opam repository for esp32 packages: 28 | `opam repo add esp32-git https://github.com/TheLortex/opam-cross-esp32.git` 29 | * Install mirage configuration tool (or update it to 3.1.0+dev): 30 | `opam install mirage` 31 | * Go in the unikernel directory that you want to run and type the following commands: 32 | ``` 33 | mirage config -t esp32 34 | make depends 35 | mirage build 36 | make flash monitor 37 | ``` 38 | 39 | You can use `make menuconfig` after the first configuration step to set up ESP32 settings, 40 | such as serial port, flash speed and size, support for external ram 41 | (mandatory for more complex unikernels as they still consume a lot of memory). 42 | 43 | ## Why 44 | 45 | I feel like people can start to test my stuff as I've been able to sort everything into packages. 46 | However the main problem remains that a Mirage unikernel barely holds onto a simple ESP32 board 47 | and often needs an external SPI RAM as the application uses an advanced network stack for example. 48 | To test these examples I recommend you to have a wrover esp32 module (the ones that has 4MB of additional RAM). 49 | -------------------------------------------------------------------------------- /ap_dhcp/config.ml: -------------------------------------------------------------------------------- 1 | open Mirage 2 | 3 | let main = 4 | foreign 5 | ~packages:[ 6 | package ~min:"0.5" ~sublibs:["server"; "wire"] "charrua-core"; 7 | package ~sublibs:["ethif"; "arpv4"] "tcpip" 8 | ] 9 | "Unikernel.Hello" (network @-> mclock @-> time @-> job) 10 | 11 | let () = 12 | register "hello" [main $ netif "ap" $ default_monotonic_clock $ default_time ] 13 | -------------------------------------------------------------------------------- /ap_dhcp/unikernel.ml: -------------------------------------------------------------------------------- 1 | open Lwt.Infix 2 | open Wifi 3 | open Mirage_types_lwt 4 | 5 | 6 | module Hello (Netif: NETWORK)(MClock: Mirage_types.MCLOCK)(Time: TIME) 7 | = struct 8 | module Eth = Ethif.Make(Netif) 9 | module Arp = Arpv4.Make(Eth)(MClock)(Time) 10 | module DC = Dhcp_config 11 | 12 | let string_of_status () = 13 | let status = Wifi.get_status () in 14 | let values = [status.inited; status.ap_started; status.sta_started; status.sta_connected] in 15 | let values_str = List.map (fun x -> if x then "T" else "F") values in 16 | "Wifi status: "^String.concat " " values_str 17 | 18 | 19 | let of_interest dest net = 20 | Macaddr.compare dest (Netif.mac net) = 0 || not (Macaddr.is_unicast dest) 21 | 22 | let input_dhcp clock net config leases buf = 23 | match Dhcp_wire.pkt_of_buf buf (Cstruct.len buf) with 24 | | Error e -> 25 | Logs.info (fun f -> f "Can't parse packet: %s" e); 26 | Lwt.return leases 27 | | Ok pkt -> 28 | let open Dhcp_server.Input in 29 | let now = MClock.elapsed_ns clock |> Duration.to_sec |> Int32.of_int in 30 | match input_pkt config leases pkt now with 31 | | Silence -> Lwt.return leases 32 | | Update leases -> 33 | Logs.info (fun f -> f "Received packet %s - updated lease database" (Dhcp_wire.pkt_to_string pkt)); 34 | Lwt.return leases 35 | | Warning w -> 36 | Logs.info (fun f -> f "%s" w); 37 | Lwt.return leases 38 | | Dhcp_server.Input.Error e -> 39 | Logs.info (fun f -> f "%s" e); 40 | Lwt.return leases 41 | | Reply (reply, leases) -> 42 | Logs.info (fun f -> f "Received packet %s" (Dhcp_wire.pkt_to_string pkt)); 43 | Netif.write net (Dhcp_wire.buf_of_pkt reply) >>= fun _ -> 44 | Logs.info (fun f -> f "Sent reply packet %s" (Dhcp_wire.pkt_to_string reply)); 45 | Lwt.return leases 46 | 47 | let start_dhcp net clock time = 48 | Eth.connect net >>= fun e -> 49 | Arp.connect e clock >>= fun a -> 50 | Arp.add_ip a DC.ip_address >>= fun () -> 51 | 52 | (* Build a dhcp server *) 53 | let config = Dhcp_server.Config.make 54 | ~hostname:DC.hostname 55 | ~default_lease_time:DC.default_lease_time 56 | ~max_lease_time:DC.max_lease_time 57 | ~hosts:DC.hosts 58 | ~addr_tuple:(DC.ip_address, Netif.mac net) 59 | ~network:DC.network 60 | ~range:DC.range 61 | ~options:DC.options 62 | in 63 | let leases = ref (Dhcp_server.Lease.make_db ()) in 64 | let listener = Netif.listen net (fun buf -> 65 | match Ethif_packet.Unmarshal.of_cstruct buf with 66 | | Result.Error s -> 67 | Logs.info (fun f -> f "Can't parse packet: %s" s); Lwt.return_unit 68 | | Result.Ok (ethif_header, ethif_payload) -> 69 | if of_interest ethif_header.Ethif_packet.destination net && 70 | Dhcp_wire.is_dhcp buf (Cstruct.len buf) then begin 71 | input_dhcp clock net config !leases buf >>= fun new_leases -> 72 | leases := new_leases; 73 | Lwt.return_unit 74 | end else if ethif_header.Ethif_packet.ethertype = Ethif_wire.ARP then 75 | Arp.input a ethif_payload 76 | else Lwt.return_unit 77 | ) in 78 | listener 79 | 80 | let start netif_ap clock time = 81 | Logs.info (fun f -> f "%s" (string_of_status ())); 82 | let _ = Wifi.start () in 83 | OS.Event.wait_for_event (Wifi.id_of_event Wifi.AP_started) >>= fun _ -> 84 | let _ = match Wifi.ap_set_config { 85 | ssid = Bytes.of_string "oui"; 86 | password = Bytes.of_string ""; 87 | channel = 1; 88 | auth_mode = Wifi.AUTH_OPEN; 89 | ssid_hidden = false; 90 | max_connection = 4; 91 | beacon_interval = 100; 92 | } with 93 | | Error _ -> Logs.info (fun f -> f "AP_set_config failed") 94 | | Ok _ -> Logs.info (fun f -> f "AP_set_config succeeded") 95 | in 96 | start_dhcp netif_ap clock time 97 | 98 | end 99 | -------------------------------------------------------------------------------- /hello_world/config.ml: -------------------------------------------------------------------------------- 1 | open Mirage 2 | 3 | let main = 4 | foreign 5 | ~packages:[package "duration"] 6 | "Unikernel.Hello" (time @-> job) 7 | 8 | let () = 9 | register "hello" [main $ default_time] 10 | -------------------------------------------------------------------------------- /hello_world/unikernel.ml: -------------------------------------------------------------------------------- 1 | open Lwt.Infix 2 | 3 | module Hello (Time : Mirage_time_lwt.S) = struct 4 | 5 | let start _time = 6 | 7 | let rec loop i = function 8 | | 0 -> Lwt.return_unit 9 | | n -> 10 | Logs.info (fun f -> f "Hello!"); 11 | Time.sleep_ns (Duration.of_sec 1) >>= fun () -> 12 | loop (i+1) (n-1) 13 | in 14 | loop 0 20 15 | 16 | end 17 | -------------------------------------------------------------------------------- /lcd_wifi_demo/config.ml: -------------------------------------------------------------------------------- 1 | open Mirage 2 | 3 | let main = 4 | foreign 5 | ~packages:[package "duration"; package "lcd"; package "wifi"] 6 | "Unikernel.Hello" (stackv4 @-> job) 7 | 8 | 9 | let () = 10 | register "hello" [main $ dyn_dhcp_ipv4_stack (netif "sta")] 11 | -------------------------------------------------------------------------------- /lcd_wifi_demo/unikernel.ml: -------------------------------------------------------------------------------- 1 | open Lwt.Infix 2 | 3 | external time : unit -> int64 = "caml_get_monotonic_time" 4 | 5 | 6 | module Hello 7 | (Stack: Mirage_stack_lwt.V4) 8 | = struct 9 | 10 | type status = { 11 | width: int; 12 | height: int; 13 | depth: int; 14 | offset: int; 15 | buffer_offset: int; 16 | image_data: int; 17 | x: int; 18 | y: int; 19 | bmp_buffer: Cstruct.t; 20 | video_buffer: Cstruct.t; 21 | } 22 | 23 | let default_status = { 24 | width = -1; 25 | height = -1; 26 | depth = -1; 27 | offset = 0; 28 | x = 20; 29 | y = 20; 30 | buffer_offset = 0; 31 | image_data = 0x10; 32 | bmp_buffer = Cstruct.create 0; 33 | video_buffer = Cstruct.create 0; 34 | } 35 | 36 | type direction = Direction_None | Direction_Up | Direction_Down | Direction_Left | Direction_Right 37 | 38 | let direction = ref Direction_None 39 | 40 | let rec handle_data buffer status = 41 | let buf_len = Cstruct.len buffer in 42 | let status, adv = match status.offset with 43 | | 0x0A -> Printf.printf "Image data start = %d\n" (Int32.to_int (Cstruct.LE.get_uint32 buffer 0x0A)); {status with image_data = Int32.to_int (Cstruct.LE.get_uint32 buffer 0x0A)}, 1 44 | | 0x12 -> Printf.printf "Width = %d\n" (Cstruct.LE.get_uint16 buffer 0x12); {status with width = Cstruct.LE.get_uint16 buffer 0x12}, 1 45 | | 0x16 -> Printf.printf "Height = %d\n" (Cstruct.LE.get_uint16 buffer 0x16); {status with height = Cstruct.LE.get_uint16 buffer 0x16}, 1 46 | | 0x18 -> {status with depth = Cstruct.LE.get_uint16 buffer 0x18}, 1 47 | | 0x19 -> Printf.printf "Allocated buffer!\n"; {status with bmp_buffer = Cstruct.create (3 * status.width * status.height)}, 1 48 | | i when i >= status.buffer_offset + buf_len -> {status with buffer_offset = status.buffer_offset + buf_len}, 0 49 | | i when i >= status.image_data -> 50 | let cval = Cstruct.get_uint8 buffer (i - status.buffer_offset) in 51 | Cstruct.set_uint8 status.bmp_buffer (i - status.image_data) cval; 52 | status, 1 53 | | _ -> status, 1 54 | in match adv with 55 | | 0 -> {status with offset = status.buffer_offset} 56 | | n -> handle_data buffer {status with offset = status.offset + n} 57 | 58 | 59 | let fill_buffer status = 60 | let status = {status with video_buffer = Cstruct.of_bigarray (Lcd.alloc_buffer (status.width*status.height))} in 61 | for x = 0 to status.width-1 do 62 | for y = 0 to status.height-1 do 63 | let linear_index = (y*(4*((status.width+3)/4)) + x)*3 in 64 | let b = (Cstruct.get_uint8 status.bmp_buffer linear_index) lsr 3 65 | and g = (Cstruct.get_uint8 status.bmp_buffer (linear_index+1)) lsr 2 66 | and r = (Cstruct.get_uint8 status.bmp_buffer (linear_index+2)) lsr 3 67 | in 68 | let pixel_int16 = (r lsl 11) + (g lsl 5) + b in 69 | Cstruct.BE.set_uint16 status.video_buffer ((y*status.width + status.width-1- x)*2) pixel_int16 70 | done; 71 | done; 72 | status 73 | 74 | let move_picture status = match !direction with 75 | | Direction_None -> status 76 | | Direction_Up -> {status with y = min (Lcd.height - status.height) (status.y + 2)} 77 | | Direction_Down -> {status with y = max 0 (status.y - 2)} 78 | | Direction_Left -> {status with x = min (Lcd.width - status.width) (status.x + 2)} 79 | | Direction_Right -> {status with x = max 0 (status.x - 2)} 80 | 81 | let refresh_image status = 82 | let status = fill_buffer status in 83 | let rec refresh status = 84 | let status = move_picture status in 85 | Lcd.transmit_buffer status.video_buffer status.x status.y status.width status.height; 86 | OS.Time.sleep_ns @@ Duration.of_ms 10 >>= (fun _ -> refresh status) 87 | in 88 | refresh status 89 | 90 | 91 | 92 | let rec read_data flow status = function 93 | | Ok `Eof -> Logs.info (fun f -> f "End of file."); refresh_image status 94 | | Ok (`Data buffer) -> Stack.TCPV4.read flow >>= read_data flow (handle_data buffer status) 95 | | Error _ -> Logs.info (fun f -> f "Connection halted during data transmission."); Lwt.return_unit 96 | 97 | let rec parse_buffer buf max_index n_read_before = function 98 | | n when n == max_index -> None 99 | | n when Cstruct.get_char buf n == '\n' -> 100 | if n_read_before == 2 then 101 | Some (Cstruct.sub buf (n+1) (max_index-n-1)) 102 | else 103 | parse_buffer buf max_index 1 (n+1) 104 | | n -> parse_buffer buf max_index (n_read_before+1) (n+1) 105 | 106 | let rec parse_header flow = function 107 | | Ok `Eof -> Logs.info (fun f -> f "End of file before reading header."); Lwt.return_unit 108 | | Ok (`Data buffer) -> 109 | begin 110 | match parse_buffer buffer (Cstruct.len buffer) 0 0 with 111 | | None -> Stack.TCPV4.read flow >>= parse_header flow 112 | | Some data_buffer -> read_data flow default_status (Ok (`Data data_buffer)) 113 | end 114 | | Error _ -> Logs.info (fun f -> f "Connection halted."); Lwt.return_unit 115 | 116 | let check_answer flow = function 117 | | Ok () -> Stack.TCPV4.read flow >>= parse_header flow 118 | | Error _ -> Logs.info (fun f -> f "Connection halted after write."); Lwt.return_unit 119 | 120 | let write_request flow = 121 | let message = "GET /ocaml.bmp HTTP/1.0\r\n"^ 122 | "Host: 192.168.43.65:8000\r\n"^ 123 | "User-Agent: esp-idf/1.0 esp32\r\n\r\n" 124 | in 125 | Stack.TCPV4.write flow (Cstruct.of_string message) >>= check_answer flow 126 | 127 | let handle_connection = function 128 | | Ok flow -> write_request flow 129 | | Error _ -> Logs.info (fun f -> f "Connection failed."); Lwt.return_unit 130 | 131 | 132 | let rec handle_client_input flow = function 133 | | Ok `Eof -> Logs.info (fun f -> f "Closing connection!"); Lwt.return_unit 134 | | Error e -> Logs.warn (fun f -> f "Error reading data from established connection: %a" Stack.TCPV4.pp_error e); Lwt.return_unit 135 | | Ok (`Data b) -> 136 | begin 137 | for i = 0 to (Cstruct.len b) - 1 do 138 | match Cstruct.get_char b i with 139 | | 'z' -> direction := Direction_Up 140 | | 'q' -> direction := Direction_Left 141 | | 's' -> direction := Direction_Down 142 | | 'd' -> direction := Direction_Right 143 | | ' ' -> direction := Direction_None 144 | | _ -> () 145 | done; 146 | Stack.TCPV4.read flow >>= handle_client_input flow 147 | end 148 | let this_is_a_server flow = 149 | let dst, dst_port = Stack.TCPV4.dst flow in 150 | Logs.info (fun f -> f "new tcp connection from IP %s on port %d" 151 | (Ipaddr.V4.to_string dst) dst_port); 152 | Stack.TCPV4.read flow >>= handle_client_input flow 153 | 154 | let start stack = 155 | (* Start wifi driver and initialize station *) 156 | let _ = Wifi.start () in 157 | OS.Event.wait_for_event (Wifi.id_of_event Wifi.STA_started) 158 | >>= fun _ -> 159 | (* Input config and wait for connection. *) 160 | let _ = Wifi.sta_set_config { 161 | ssid=Bytes.of_string "not a wifi"; 162 | password=Bytes.of_string "not a password"; 163 | } in 164 | let _ = Wifi.connect () in 165 | OS.Time.sleep_ns @@ Duration.of_sec 8 >>= fun _ -> 166 | let ip = Ipaddr.V4.of_string_exn "192.168.43.65" 167 | and port = 8000 168 | in 169 | Stack.listen_tcpv4 stack 8000 this_is_a_server; 170 | let image_downloader = Stack.TCPV4.create_connection (Stack.tcpv4 stack) (ip, port) >>= handle_connection 171 | and command_server = Stack.listen stack in 172 | Lwt.join [image_downloader; command_server] 173 | 174 | end 175 | --------------------------------------------------------------------------------